This commit is contained in:
216
internal/services/iscsi.go
Normal file
216
internal/services/iscsi.go
Normal 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
145
internal/services/nfs.go
Normal 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
167
internal/services/smb.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user