add storage service
Some checks failed
CI / test-build (push) Failing after 2m4s

This commit is contained in:
2025-12-15 00:01:05 +07:00
parent 54e76d9304
commit 7c33e736f9
6 changed files with 768 additions and 0 deletions

216
internal/services/iscsi.go Normal file
View File

@@ -0,0 +1,216 @@
package services
import (
"fmt"
"os/exec"
"strings"
"sync"
"gitea.avt.data-center.id/othman.suseno/atlas/internal/models"
)
// ISCSIService manages iSCSI target service integration
type ISCSIService struct {
mu sync.RWMutex
targetcliPath string
}
// NewISCSIService creates a new iSCSI service manager
func NewISCSIService() *ISCSIService {
return &ISCSIService{
targetcliPath: "targetcli",
}
}
// ApplyConfiguration applies iSCSI target configurations
func (s *ISCSIService) ApplyConfiguration(targets []models.ISCSITarget) error {
s.mu.Lock()
defer s.mu.Unlock()
// For each target, ensure it exists and is configured
for _, target := range targets {
if !target.Enabled {
// Disable target if it exists
if err := s.disableTarget(target.IQN); err != nil {
// Log but continue
}
continue
}
// Create or update target
if err := s.createTarget(target); err != nil {
return fmt.Errorf("create target %s: %w", target.IQN, err)
}
// Configure ACLs
if err := s.configureACLs(target); err != nil {
return fmt.Errorf("configure ACLs for %s: %w", target.IQN, err)
}
// Configure LUNs
if err := s.configureLUNs(target); err != nil {
return fmt.Errorf("configure LUNs for %s: %w", target.IQN, err)
}
}
return nil
}
// createTarget creates an iSCSI target
func (s *ISCSIService) createTarget(target models.ISCSITarget) error {
// Use targetcli to create target
// Format: targetcli /iscsi create <IQN>
cmd := exec.Command(s.targetcliPath, "/iscsi", "create", target.IQN)
if err := cmd.Run(); err != nil {
// Target might already exist, which is OK
// Check if it actually exists
if !s.targetExists(target.IQN) {
return fmt.Errorf("create target failed: %w", err)
}
}
return nil
}
// configureACLs configures initiator ACLs for a target
func (s *ISCSIService) configureACLs(target models.ISCSITarget) error {
// Get current ACLs
currentACLs, _ := s.getACLs(target.IQN)
// Remove ACLs not in desired list
for _, acl := range currentACLs {
if !contains(target.Initiators, acl) {
cmd := exec.Command(s.targetcliPath, "/iscsi/"+target.IQN+"/tpg1/acls", "delete", acl)
cmd.Run() // Ignore errors
}
}
// Add new ACLs
for _, initiator := range target.Initiators {
if !contains(currentACLs, initiator) {
cmd := exec.Command(s.targetcliPath, "/iscsi/"+target.IQN+"/tpg1/acls", "create", initiator)
if err := cmd.Run(); err != nil {
return fmt.Errorf("create ACL %s: %w", initiator, err)
}
}
}
return nil
}
// configureLUNs configures LUNs for a target
func (s *ISCSIService) configureLUNs(target models.ISCSITarget) error {
// Get current LUNs
currentLUNs, _ := s.getLUNs(target.IQN)
// Remove LUNs not in desired list
for _, lun := range currentLUNs {
if !s.hasLUN(target.LUNs, lun) {
cmd := exec.Command(s.targetcliPath, "/iscsi/"+target.IQN+"/tpg1/luns", "delete", fmt.Sprintf("lun/%d", lun))
cmd.Run() // Ignore errors
}
}
// Add/update LUNs
for _, lun := range target.LUNs {
// Create LUN mapping
// Format: targetcli /iscsi/<IQN>/tpg1/luns create /backstores/zvol/<zvol>
zvolPath := "/backstores/zvol/" + lun.ZVOL
// First ensure the zvol backend exists
cmd := exec.Command(s.targetcliPath, "/backstores/zvol", "create", lun.ZVOL, lun.ZVOL)
cmd.Run() // Ignore if already exists
// Create LUN
cmd = exec.Command(s.targetcliPath, "/iscsi/"+target.IQN+"/tpg1/luns", "create", zvolPath)
if err := cmd.Run(); err != nil {
// LUN might already exist
if !s.hasLUNID(currentLUNs, lun.ID) {
return fmt.Errorf("create LUN %d: %w", lun.ID, err)
}
}
}
return nil
}
// Helper functions
func (s *ISCSIService) targetExists(iqn string) bool {
cmd := exec.Command(s.targetcliPath, "/iscsi", "ls")
output, err := cmd.Output()
if err != nil {
return false
}
return strings.Contains(string(output), iqn)
}
func (s *ISCSIService) getACLs(iqn string) ([]string, error) {
cmd := exec.Command(s.targetcliPath, "/iscsi/"+iqn+"/tpg1/acls", "ls")
_, err := cmd.Output()
if err != nil {
return nil, err
}
// Parse output to extract ACL names
// This is simplified - real implementation would parse targetcli output
return []string{}, nil
}
func (s *ISCSIService) getLUNs(iqn string) ([]int, error) {
cmd := exec.Command(s.targetcliPath, "/iscsi/"+iqn+"/tpg1/luns", "ls")
_, err := cmd.Output()
if err != nil {
return nil, err
}
// Parse output to extract LUN IDs
// This is simplified - real implementation would parse targetcli output
return []int{}, nil
}
func (s *ISCSIService) hasLUN(luns []models.LUN, id int) bool {
for _, lun := range luns {
if lun.ID == id {
return true
}
}
return false
}
func (s *ISCSIService) hasLUNID(luns []int, id int) bool {
for _, lunID := range luns {
if lunID == id {
return true
}
}
return false
}
func (s *ISCSIService) disableTarget(iqn string) error {
cmd := exec.Command(s.targetcliPath, "/iscsi/"+iqn+"/tpg1", "set", "attribute", "enable=0")
return cmd.Run()
}
// GetStatus returns the status of iSCSI target service
func (s *ISCSIService) GetStatus() (bool, error) {
// Check if targetd is running
cmd := exec.Command("systemctl", "is-active", "target")
if err := cmd.Run(); err == nil {
return true, nil
}
// Fallback: check process
cmd = exec.Command("pgrep", "-x", "targetd")
if err := cmd.Run(); err == nil {
return true, nil
}
return false, nil
}
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}

145
internal/services/nfs.go Normal file
View File

@@ -0,0 +1,145 @@
package services
import (
"fmt"
"os"
"os/exec"
"strings"
"sync"
"gitea.avt.data-center.id/othman.suseno/atlas/internal/models"
)
// NFSService manages NFS service integration
type NFSService struct {
mu sync.RWMutex
exportsPath string
}
// NewNFSService creates a new NFS service manager
func NewNFSService() *NFSService {
return &NFSService{
exportsPath: "/etc/exports",
}
}
// ApplyConfiguration generates and applies NFS exports configuration
func (s *NFSService) ApplyConfiguration(exports []models.NFSExport) error {
s.mu.Lock()
defer s.mu.Unlock()
config, err := s.generateExports(exports)
if err != nil {
return fmt.Errorf("generate exports: %w", err)
}
// Write configuration to a temporary file first
tmpPath := s.exportsPath + ".atlas.tmp"
if err := os.WriteFile(tmpPath, []byte(config), 0644); err != nil {
return fmt.Errorf("write exports: %w", err)
}
// Backup existing exports
backupPath := s.exportsPath + ".backup"
if _, err := os.Stat(s.exportsPath); err == nil {
if err := exec.Command("cp", s.exportsPath, backupPath).Run(); err != nil {
// Non-fatal, log but continue
}
}
// Atomically replace exports file
if err := os.Rename(tmpPath, s.exportsPath); err != nil {
return fmt.Errorf("replace exports: %w", err)
}
// Reload NFS exports
if err := s.reloadExports(); err != nil {
// Try to restore backup on failure
if _, err2 := os.Stat(backupPath); err2 == nil {
os.Rename(backupPath, s.exportsPath)
}
return fmt.Errorf("reload exports: %w", err)
}
return nil
}
// generateExports generates /etc/exports format from NFS exports
func (s *NFSService) generateExports(exports []models.NFSExport) (string, error) {
var b strings.Builder
for _, export := range exports {
if !export.Enabled {
continue
}
// Build export options
var options []string
if export.ReadOnly {
options = append(options, "ro")
} else {
options = append(options, "rw")
}
if export.RootSquash {
options = append(options, "root_squash")
} else {
options = append(options, "no_root_squash")
}
options = append(options, "sync", "subtree_check")
// Format: path client1(options) client2(options)
optStr := "(" + strings.Join(options, ",") + ")"
if len(export.Clients) == 0 {
// Default to all clients if none specified
b.WriteString(fmt.Sprintf("%s *%s\n", export.Path, optStr))
} else {
for _, client := range export.Clients {
b.WriteString(fmt.Sprintf("%s %s%s\n", export.Path, client, optStr))
}
}
}
return b.String(), nil
}
// reloadExports reloads NFS exports
func (s *NFSService) reloadExports() error {
// Use exportfs -ra to reload all exports
cmd := exec.Command("exportfs", "-ra")
if err := cmd.Run(); err != nil {
return fmt.Errorf("exportfs failed: %w", err)
}
return nil
}
// ValidateConfiguration validates NFS exports syntax
func (s *NFSService) ValidateConfiguration(exports string) error {
// Use exportfs -v to validate (dry-run)
cmd := exec.Command("exportfs", "-v")
cmd.Stdin = strings.NewReader(exports)
// Note: exportfs doesn't have a direct validation mode
// We'll rely on the reload to catch errors
return nil
}
// GetStatus returns the status of NFS service
func (s *NFSService) GetStatus() (bool, error) {
// Check if nfs-server is running
cmd := exec.Command("systemctl", "is-active", "nfs-server")
if err := cmd.Run(); err == nil {
return true, nil
}
// Fallback: check process
cmd = exec.Command("pgrep", "-x", "nfsd")
if err := cmd.Run(); err == nil {
return true, nil
}
return false, nil
}

167
internal/services/smb.go Normal file
View File

@@ -0,0 +1,167 @@
package services
import (
"fmt"
"os"
"os/exec"
"strings"
"sync"
"gitea.avt.data-center.id/othman.suseno/atlas/internal/models"
)
// SMBService manages Samba service integration
type SMBService struct {
mu sync.RWMutex
configPath string
smbConfPath string
smbctlPath string
}
// NewSMBService creates a new SMB service manager
func NewSMBService() *SMBService {
return &SMBService{
configPath: "/etc/samba/smb.conf",
smbctlPath: "smbcontrol",
}
}
// ApplyConfiguration generates and applies SMB configuration
func (s *SMBService) ApplyConfiguration(shares []models.SMBShare) error {
s.mu.Lock()
defer s.mu.Unlock()
config, err := s.generateConfig(shares)
if err != nil {
return fmt.Errorf("generate config: %w", err)
}
// Write configuration to a temporary file first
tmpPath := s.configPath + ".atlas.tmp"
if err := os.WriteFile(tmpPath, []byte(config), 0644); err != nil {
return fmt.Errorf("write config: %w", err)
}
// Backup existing config
backupPath := s.configPath + ".backup"
if _, err := os.Stat(s.configPath); err == nil {
if err := exec.Command("cp", s.configPath, backupPath).Run(); err != nil {
// Non-fatal, log but continue
}
}
// Atomically replace config
if err := os.Rename(tmpPath, s.configPath); err != nil {
return fmt.Errorf("replace config: %w", err)
}
// Reload Samba service
if err := s.reloadService(); err != nil {
// Try to restore backup on failure
if _, err2 := os.Stat(backupPath); err2 == nil {
os.Rename(backupPath, s.configPath)
}
return fmt.Errorf("reload service: %w", err)
}
return nil
}
// generateConfig generates Samba configuration from shares
func (s *SMBService) generateConfig(shares []models.SMBShare) (string, error) {
var b strings.Builder
// Global section
b.WriteString("[global]\n")
b.WriteString(" workgroup = WORKGROUP\n")
b.WriteString(" server string = AtlasOS Storage Server\n")
b.WriteString(" security = user\n")
b.WriteString(" map to guest = Bad User\n")
b.WriteString(" dns proxy = no\n")
b.WriteString("\n")
// Share sections
for _, share := range shares {
if !share.Enabled {
continue
}
b.WriteString(fmt.Sprintf("[%s]\n", share.Name))
b.WriteString(fmt.Sprintf(" path = %s\n", share.Path))
if share.Description != "" {
b.WriteString(fmt.Sprintf(" comment = %s\n", share.Description))
}
if share.ReadOnly {
b.WriteString(" read only = yes\n")
} else {
b.WriteString(" read only = no\n")
b.WriteString(" writable = yes\n")
}
if share.GuestOK {
b.WriteString(" guest ok = yes\n")
b.WriteString(" public = yes\n")
} else {
b.WriteString(" guest ok = no\n")
}
if len(share.ValidUsers) > 0 {
b.WriteString(fmt.Sprintf(" valid users = %s\n", strings.Join(share.ValidUsers, ", ")))
}
b.WriteString(" browseable = yes\n")
b.WriteString("\n")
}
return b.String(), nil
}
// reloadService reloads Samba configuration
func (s *SMBService) reloadService() error {
// Try smbcontrol first (doesn't require root for reload)
cmd := exec.Command(s.smbctlPath, "all", "reload-config")
if err := cmd.Run(); err == nil {
return nil
}
// Fallback to systemctl if available
cmd = exec.Command("systemctl", "reload", "smbd")
if err := cmd.Run(); err == nil {
return nil
}
// Try service command
cmd = exec.Command("service", "smbd", "reload")
if err := cmd.Run(); err == nil {
return nil
}
return fmt.Errorf("unable to reload Samba service")
}
// ValidateConfiguration validates SMB configuration syntax
func (s *SMBService) ValidateConfiguration(config string) error {
// Use testparm to validate configuration
cmd := exec.Command("testparm", "-s")
cmd.Stdin = strings.NewReader(config)
if err := cmd.Run(); err != nil {
return fmt.Errorf("configuration validation failed: %w", err)
}
return nil
}
// GetStatus returns the status of Samba service
func (s *SMBService) GetStatus() (bool, error) {
// Check if smbd is running
cmd := exec.Command("systemctl", "is-active", "smbd")
if err := cmd.Run(); err == nil {
return true, nil
}
// Fallback: check process
cmd = exec.Command("pgrep", "-x", "smbd")
if err := cmd.Run(); err == nil {
return true, nil
}
return false, nil
}