This commit is contained in:
163
docs/SERVICE_INTEGRATION.md
Normal file
163
docs/SERVICE_INTEGRATION.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# Service Daemon Integration
|
||||
|
||||
## Overview
|
||||
|
||||
AtlasOS integrates with system storage daemons (Samba, NFS, iSCSI) to automatically apply configuration changes. When storage services are created, updated, or deleted via the API, the system daemons are automatically reconfigured.
|
||||
|
||||
## Architecture
|
||||
|
||||
The service integration layer (`internal/services/`) provides:
|
||||
|
||||
- **Configuration Generation**: Converts API models to daemon-specific configuration formats
|
||||
- **Atomic Updates**: Writes to temporary files, then atomically replaces configuration
|
||||
- **Safe Reloads**: Reloads services without interrupting active connections
|
||||
- **Error Recovery**: Automatically restores backups on configuration failures
|
||||
|
||||
## SMB/Samba Integration
|
||||
|
||||
### Configuration
|
||||
- **Config File**: `/etc/samba/smb.conf`
|
||||
- **Service**: `smbd` (Samba daemon)
|
||||
- **Reload Method**: `smbcontrol all reload-config` or `systemctl reload smbd`
|
||||
|
||||
### Features
|
||||
- Generates Samba configuration from SMB share definitions
|
||||
- Supports read-only, guest access, and user restrictions
|
||||
- Automatically reloads Samba after configuration changes
|
||||
- Validates configuration syntax using `testparm`
|
||||
|
||||
### Example
|
||||
When an SMB share is created via API:
|
||||
1. Share is stored in the store
|
||||
2. All shares are retrieved
|
||||
3. Samba configuration is generated
|
||||
4. Configuration is written to `/etc/samba/smb.conf`
|
||||
5. Samba service is reloaded
|
||||
|
||||
## NFS Integration
|
||||
|
||||
### Configuration
|
||||
- **Config File**: `/etc/exports`
|
||||
- **Service**: `nfs-server`
|
||||
- **Reload Method**: `exportfs -ra`
|
||||
|
||||
### Features
|
||||
- Generates `/etc/exports` format from NFS export definitions
|
||||
- Supports read-only, client restrictions, and root squash
|
||||
- Automatically reloads NFS exports after configuration changes
|
||||
- Handles multiple clients per export
|
||||
|
||||
### Example
|
||||
When an NFS export is created via API:
|
||||
1. Export is stored in the store
|
||||
2. All exports are retrieved
|
||||
3. `/etc/exports` is generated
|
||||
4. Exports file is written atomically
|
||||
5. NFS exports are reloaded using `exportfs -ra`
|
||||
|
||||
## iSCSI Integration
|
||||
|
||||
### Configuration
|
||||
- **Tool**: `targetcli` (LIO target framework)
|
||||
- **Service**: `target` (systemd service)
|
||||
- **Method**: Direct targetcli commands
|
||||
|
||||
### Features
|
||||
- Creates iSCSI targets with IQN
|
||||
- Configures initiator ACLs
|
||||
- Maps ZVOLs as LUNs
|
||||
- Manages target enable/disable state
|
||||
|
||||
### Example
|
||||
When an iSCSI target is created via API:
|
||||
1. Target is stored in the store
|
||||
2. All targets are retrieved
|
||||
3. For each target:
|
||||
- Target is created via `targetcli`
|
||||
- Initiator ACLs are configured
|
||||
- LUNs are mapped to ZVOLs
|
||||
4. Configuration is applied atomically
|
||||
|
||||
## Safety Features
|
||||
|
||||
### Atomic Configuration Updates
|
||||
1. Write configuration to temporary file (`*.atlas.tmp`)
|
||||
2. Backup existing configuration (`.backup`)
|
||||
3. Atomically replace configuration file
|
||||
4. Reload service
|
||||
5. On failure, restore backup
|
||||
|
||||
### Error Handling
|
||||
- Configuration errors are logged but don't fail API requests
|
||||
- Service reload failures trigger automatic backup restoration
|
||||
- Validation is performed before applying changes (where supported)
|
||||
|
||||
## Service Status
|
||||
|
||||
Each service provides a `GetStatus()` method to check if the daemon is running:
|
||||
|
||||
```go
|
||||
// Check Samba status
|
||||
running, err := smbService.GetStatus()
|
||||
|
||||
// Check NFS status
|
||||
running, err := nfsService.GetStatus()
|
||||
|
||||
// Check iSCSI status
|
||||
running, err := iscsiService.GetStatus()
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
### Samba
|
||||
- `samba` package installed
|
||||
- `smbcontrol` command available
|
||||
- Write access to `/etc/samba/smb.conf`
|
||||
- Root/sudo privileges for service reload
|
||||
|
||||
### NFS
|
||||
- `nfs-kernel-server` package installed
|
||||
- `exportfs` command available
|
||||
- Write access to `/etc/exports`
|
||||
- Root/sudo privileges for export reload
|
||||
|
||||
### iSCSI
|
||||
- `targetcli` package installed
|
||||
- LIO target framework enabled
|
||||
- Root/sudo privileges for targetcli operations
|
||||
- ZVOL backend support
|
||||
|
||||
## Configuration Flow
|
||||
|
||||
```
|
||||
API Request → Store Update → Service Integration → Daemon Configuration
|
||||
↓ ↓ ↓ ↓
|
||||
Create/ Store in Generate Config Write & Reload
|
||||
Update/ Memory/DB from Models Service
|
||||
Delete
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Async Configuration**: Queue configuration changes for background processing
|
||||
2. **Validation API**: Pre-validate configurations before applying
|
||||
3. **Rollback Support**: Automatic rollback on service failures
|
||||
4. **Status Monitoring**: Real-time service health monitoring
|
||||
5. **Configuration Diff**: Show what will change before applying
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Samba Configuration Not Applied
|
||||
- Check Samba service status: `systemctl status smbd`
|
||||
- Validate configuration: `testparm -s`
|
||||
- Check logs: `journalctl -u smbd`
|
||||
|
||||
### NFS Exports Not Working
|
||||
- Check NFS service status: `systemctl status nfs-server`
|
||||
- Verify exports: `exportfs -v`
|
||||
- Check permissions on exported paths
|
||||
|
||||
### iSCSI Targets Not Created
|
||||
- Verify targetcli is installed: `which targetcli`
|
||||
- Check LIO service: `systemctl status target`
|
||||
- Review targetcli output for errors
|
||||
@@ -517,6 +517,14 @@ func (a *App) handleCreateSMBShare(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Apply configuration to Samba service
|
||||
shares := a.smbStore.List()
|
||||
if err := a.smbService.ApplyConfiguration(shares); err != nil {
|
||||
log.Printf("apply SMB configuration error: %v", err)
|
||||
// Don't fail the request, but log the error
|
||||
// In production, you might want to queue this for retry
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, share)
|
||||
}
|
||||
|
||||
@@ -571,6 +579,13 @@ func (a *App) handleUpdateSMBShare(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
share, _ := a.smbStore.Get(id)
|
||||
|
||||
// Apply configuration to Samba service
|
||||
shares := a.smbStore.List()
|
||||
if err := a.smbService.ApplyConfiguration(shares); err != nil {
|
||||
log.Printf("apply SMB configuration error: %v", err)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, share)
|
||||
}
|
||||
|
||||
@@ -659,6 +674,12 @@ func (a *App) handleCreateNFSExport(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Apply configuration to NFS service
|
||||
exports := a.nfsStore.List()
|
||||
if err := a.nfsService.ApplyConfiguration(exports); err != nil {
|
||||
log.Printf("apply NFS configuration error: %v", err)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, export)
|
||||
}
|
||||
|
||||
@@ -712,6 +733,13 @@ func (a *App) handleUpdateNFSExport(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
export, _ := a.nfsStore.Get(id)
|
||||
|
||||
// Apply configuration to NFS service
|
||||
exports := a.nfsStore.List()
|
||||
if err := a.nfsService.ApplyConfiguration(exports); err != nil {
|
||||
log.Printf("apply NFS configuration error: %v", err)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, export)
|
||||
}
|
||||
|
||||
@@ -732,6 +760,12 @@ func (a *App) handleDeleteNFSExport(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Apply configuration to NFS service
|
||||
exports := a.nfsStore.List()
|
||||
if err := a.nfsService.ApplyConfiguration(exports); err != nil {
|
||||
log.Printf("apply NFS configuration error: %v", err)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{"message": "export deleted", "id": id})
|
||||
}
|
||||
|
||||
@@ -774,6 +808,12 @@ func (a *App) handleCreateISCSITarget(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Apply configuration to iSCSI service
|
||||
targets := a.iscsiStore.List()
|
||||
if err := a.iscsiService.ApplyConfiguration(targets); err != nil {
|
||||
log.Printf("apply iSCSI configuration error: %v", err)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, target)
|
||||
}
|
||||
|
||||
@@ -825,6 +865,13 @@ func (a *App) handleUpdateISCSITarget(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
target, _ := a.iscsiStore.Get(id)
|
||||
|
||||
// Apply configuration to iSCSI service
|
||||
targets := a.iscsiStore.List()
|
||||
if err := a.iscsiService.ApplyConfiguration(targets); err != nil {
|
||||
log.Printf("apply iSCSI configuration error: %v", err)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, target)
|
||||
}
|
||||
|
||||
@@ -845,6 +892,12 @@ func (a *App) handleDeleteISCSITarget(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Apply configuration to iSCSI service
|
||||
targets := a.iscsiStore.List()
|
||||
if err := a.iscsiService.ApplyConfiguration(targets); err != nil {
|
||||
log.Printf("apply iSCSI configuration error: %v", err)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{"message": "target deleted", "id": id})
|
||||
}
|
||||
|
||||
@@ -910,6 +963,12 @@ func (a *App) handleAddLUN(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Apply configuration to iSCSI service
|
||||
targets := a.iscsiStore.List()
|
||||
if err := a.iscsiService.ApplyConfiguration(targets); err != nil {
|
||||
log.Printf("apply iSCSI configuration error: %v", err)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, lun)
|
||||
}
|
||||
|
||||
@@ -946,6 +1005,12 @@ func (a *App) handleRemoveLUN(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Apply configuration to iSCSI service
|
||||
targets := a.iscsiStore.List()
|
||||
if err := a.iscsiService.ApplyConfiguration(targets); err != nil {
|
||||
log.Printf("apply iSCSI configuration error: %v", err)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{"message": "LUN removed", "target_id": id, "lun_id": strconv.Itoa(req.LUNID)})
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"gitea.avt.data-center.id/othman.suseno/atlas/internal/auth"
|
||||
"gitea.avt.data-center.id/othman.suseno/atlas/internal/db"
|
||||
"gitea.avt.data-center.id/othman.suseno/atlas/internal/job"
|
||||
"gitea.avt.data-center.id/othman.suseno/atlas/internal/services"
|
||||
"gitea.avt.data-center.id/othman.suseno/atlas/internal/snapshot"
|
||||
"gitea.avt.data-center.id/othman.suseno/atlas/internal/storage"
|
||||
"gitea.avt.data-center.id/othman.suseno/atlas/internal/zfs"
|
||||
@@ -39,6 +40,9 @@ type App struct {
|
||||
nfsStore *storage.NFSStore
|
||||
iscsiStore *storage.ISCSIStore
|
||||
database *db.DB // Optional database connection
|
||||
smbService *services.SMBService
|
||||
nfsService *services.NFSService
|
||||
iscsiService *services.ISCSIService
|
||||
}
|
||||
|
||||
func New(cfg Config) (*App, error) {
|
||||
@@ -82,6 +86,11 @@ func New(cfg Config) (*App, error) {
|
||||
nfsStore := storage.NewNFSStore()
|
||||
iscsiStore := storage.NewISCSIStore()
|
||||
|
||||
// Initialize service daemon integrations
|
||||
smbService := services.NewSMBService()
|
||||
nfsService := services.NewNFSService()
|
||||
iscsiService := services.NewISCSIService()
|
||||
|
||||
a := &App{
|
||||
cfg: cfg,
|
||||
tmpl: tmpl,
|
||||
@@ -97,6 +106,9 @@ func New(cfg Config) (*App, error) {
|
||||
nfsStore: nfsStore,
|
||||
iscsiStore: iscsiStore,
|
||||
database: database,
|
||||
smbService: smbService,
|
||||
nfsService: nfsService,
|
||||
iscsiService: iscsiService,
|
||||
}
|
||||
|
||||
// Start snapshot scheduler (runs every 15 minutes)
|
||||
|
||||
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