807 lines
23 KiB
Go
807 lines
23 KiB
Go
package shares
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/atlasos/calypso/internal/common/database"
|
|
"github.com/atlasos/calypso/internal/common/logger"
|
|
"github.com/lib/pq"
|
|
)
|
|
|
|
// Service handles Shares (CIFS/NFS) operations
|
|
type Service struct {
|
|
db *database.DB
|
|
logger *logger.Logger
|
|
}
|
|
|
|
// NewService creates a new Shares service
|
|
func NewService(db *database.DB, log *logger.Logger) *Service {
|
|
return &Service{
|
|
db: db,
|
|
logger: log,
|
|
}
|
|
}
|
|
|
|
// Share represents a filesystem share (NFS/SMB)
|
|
type Share struct {
|
|
ID string `json:"id"`
|
|
DatasetID string `json:"dataset_id"`
|
|
DatasetName string `json:"dataset_name"`
|
|
MountPoint string `json:"mount_point"`
|
|
ShareType string `json:"share_type"` // 'nfs', 'smb', 'both'
|
|
NFSEnabled bool `json:"nfs_enabled"`
|
|
NFSOptions string `json:"nfs_options,omitempty"`
|
|
NFSClients []string `json:"nfs_clients,omitempty"`
|
|
SMBEnabled bool `json:"smb_enabled"`
|
|
SMBShareName string `json:"smb_share_name,omitempty"`
|
|
SMBPath string `json:"smb_path,omitempty"`
|
|
SMBComment string `json:"smb_comment,omitempty"`
|
|
SMBGuestOK bool `json:"smb_guest_ok"`
|
|
SMBReadOnly bool `json:"smb_read_only"`
|
|
SMBBrowseable bool `json:"smb_browseable"`
|
|
IsActive bool `json:"is_active"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
CreatedBy string `json:"created_by"`
|
|
}
|
|
|
|
// ListShares lists all shares
|
|
func (s *Service) ListShares(ctx context.Context) ([]*Share, error) {
|
|
query := `
|
|
SELECT
|
|
zs.id, zs.dataset_id, zd.name as dataset_name, zd.mount_point,
|
|
zs.share_type, zs.nfs_enabled, zs.nfs_options, zs.nfs_clients,
|
|
zs.smb_enabled, zs.smb_share_name, zs.smb_path, zs.smb_comment,
|
|
zs.smb_guest_ok, zs.smb_read_only, zs.smb_browseable,
|
|
zs.is_active, zs.created_at, zs.updated_at, zs.created_by
|
|
FROM zfs_shares zs
|
|
JOIN zfs_datasets zd ON zs.dataset_id = zd.id
|
|
ORDER BY zd.name
|
|
`
|
|
|
|
rows, err := s.db.QueryContext(ctx, query)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "does not exist") {
|
|
s.logger.Warn("zfs_shares table does not exist, returning empty list")
|
|
return []*Share{}, nil
|
|
}
|
|
return nil, fmt.Errorf("failed to list shares: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var shares []*Share
|
|
for rows.Next() {
|
|
var share Share
|
|
var mountPoint sql.NullString
|
|
var nfsOptions sql.NullString
|
|
var smbShareName sql.NullString
|
|
var smbPath sql.NullString
|
|
var smbComment sql.NullString
|
|
var nfsClients []string
|
|
|
|
err := rows.Scan(
|
|
&share.ID, &share.DatasetID, &share.DatasetName, &mountPoint,
|
|
&share.ShareType, &share.NFSEnabled, &nfsOptions, pq.Array(&nfsClients),
|
|
&share.SMBEnabled, &smbShareName, &smbPath, &smbComment,
|
|
&share.SMBGuestOK, &share.SMBReadOnly, &share.SMBBrowseable,
|
|
&share.IsActive, &share.CreatedAt, &share.UpdatedAt, &share.CreatedBy,
|
|
)
|
|
|
|
if err != nil {
|
|
s.logger.Error("Failed to scan share row", "error", err)
|
|
continue
|
|
}
|
|
|
|
share.NFSClients = nfsClients
|
|
|
|
if mountPoint.Valid {
|
|
share.MountPoint = mountPoint.String
|
|
}
|
|
if nfsOptions.Valid {
|
|
share.NFSOptions = nfsOptions.String
|
|
}
|
|
if smbShareName.Valid {
|
|
share.SMBShareName = smbShareName.String
|
|
}
|
|
if smbPath.Valid {
|
|
share.SMBPath = smbPath.String
|
|
}
|
|
if smbComment.Valid {
|
|
share.SMBComment = smbComment.String
|
|
}
|
|
|
|
shares = append(shares, &share)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating share rows: %w", err)
|
|
}
|
|
|
|
return shares, nil
|
|
}
|
|
|
|
// GetShare retrieves a share by ID
|
|
func (s *Service) GetShare(ctx context.Context, shareID string) (*Share, error) {
|
|
query := `
|
|
SELECT
|
|
zs.id, zs.dataset_id, zd.name as dataset_name, zd.mount_point,
|
|
zs.share_type, zs.nfs_enabled, zs.nfs_options, zs.nfs_clients,
|
|
zs.smb_enabled, zs.smb_share_name, zs.smb_path, zs.smb_comment,
|
|
zs.smb_guest_ok, zs.smb_read_only, zs.smb_browseable,
|
|
zs.is_active, zs.created_at, zs.updated_at, zs.created_by
|
|
FROM zfs_shares zs
|
|
JOIN zfs_datasets zd ON zs.dataset_id = zd.id
|
|
WHERE zs.id = $1
|
|
`
|
|
|
|
var share Share
|
|
var mountPoint sql.NullString
|
|
var nfsOptions sql.NullString
|
|
var smbShareName sql.NullString
|
|
var smbPath sql.NullString
|
|
var smbComment sql.NullString
|
|
var nfsClients []string
|
|
|
|
err := s.db.QueryRowContext(ctx, query, shareID).Scan(
|
|
&share.ID, &share.DatasetID, &share.DatasetName, &mountPoint,
|
|
&share.ShareType, &share.NFSEnabled, &nfsOptions, pq.Array(&nfsClients),
|
|
&share.SMBEnabled, &smbShareName, &smbPath, &smbComment,
|
|
&share.SMBGuestOK, &share.SMBReadOnly, &share.SMBBrowseable,
|
|
&share.IsActive, &share.CreatedAt, &share.UpdatedAt, &share.CreatedBy,
|
|
)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("share not found")
|
|
}
|
|
return nil, fmt.Errorf("failed to get share: %w", err)
|
|
}
|
|
|
|
share.NFSClients = nfsClients
|
|
|
|
if mountPoint.Valid {
|
|
share.MountPoint = mountPoint.String
|
|
}
|
|
if nfsOptions.Valid {
|
|
share.NFSOptions = nfsOptions.String
|
|
}
|
|
if smbShareName.Valid {
|
|
share.SMBShareName = smbShareName.String
|
|
}
|
|
if smbPath.Valid {
|
|
share.SMBPath = smbPath.String
|
|
}
|
|
if smbComment.Valid {
|
|
share.SMBComment = smbComment.String
|
|
}
|
|
|
|
return &share, nil
|
|
}
|
|
|
|
// CreateShareRequest represents a share creation request
|
|
type CreateShareRequest struct {
|
|
DatasetID string `json:"dataset_id" binding:"required"`
|
|
NFSEnabled bool `json:"nfs_enabled"`
|
|
NFSOptions string `json:"nfs_options"`
|
|
NFSClients []string `json:"nfs_clients"`
|
|
SMBEnabled bool `json:"smb_enabled"`
|
|
SMBShareName string `json:"smb_share_name"`
|
|
SMBPath string `json:"smb_path"`
|
|
SMBComment string `json:"smb_comment"`
|
|
SMBGuestOK bool `json:"smb_guest_ok"`
|
|
SMBReadOnly bool `json:"smb_read_only"`
|
|
SMBBrowseable bool `json:"smb_browseable"`
|
|
}
|
|
|
|
// CreateShare creates a new share
|
|
func (s *Service) CreateShare(ctx context.Context, req *CreateShareRequest, userID string) (*Share, error) {
|
|
// Validate dataset exists and is a filesystem (not volume)
|
|
// req.DatasetID can be either UUID or dataset name
|
|
var datasetID, datasetType, datasetName, mountPoint string
|
|
var mountPointNull sql.NullString
|
|
|
|
// Try to find by ID first (UUID)
|
|
err := s.db.QueryRowContext(ctx,
|
|
"SELECT id, type, name, mount_point FROM zfs_datasets WHERE id = $1",
|
|
req.DatasetID,
|
|
).Scan(&datasetID, &datasetType, &datasetName, &mountPointNull)
|
|
|
|
// If not found by ID, try by name
|
|
if err == sql.ErrNoRows {
|
|
err = s.db.QueryRowContext(ctx,
|
|
"SELECT id, type, name, mount_point FROM zfs_datasets WHERE name = $1",
|
|
req.DatasetID,
|
|
).Scan(&datasetID, &datasetType, &datasetName, &mountPointNull)
|
|
}
|
|
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("dataset not found")
|
|
}
|
|
return nil, fmt.Errorf("failed to validate dataset: %w", err)
|
|
}
|
|
|
|
if mountPointNull.Valid {
|
|
mountPoint = mountPointNull.String
|
|
} else {
|
|
mountPoint = "none"
|
|
}
|
|
|
|
if datasetType != "filesystem" {
|
|
return nil, fmt.Errorf("only filesystem datasets can be shared (not volumes)")
|
|
}
|
|
|
|
// Determine share type
|
|
shareType := "none"
|
|
if req.NFSEnabled && req.SMBEnabled {
|
|
shareType = "both"
|
|
} else if req.NFSEnabled {
|
|
shareType = "nfs"
|
|
} else if req.SMBEnabled {
|
|
shareType = "smb"
|
|
} else {
|
|
return nil, fmt.Errorf("at least one protocol (NFS or SMB) must be enabled")
|
|
}
|
|
|
|
// Set default NFS options if not provided
|
|
nfsOptions := req.NFSOptions
|
|
if nfsOptions == "" {
|
|
nfsOptions = "rw,sync,no_subtree_check"
|
|
}
|
|
|
|
// Set default SMB share name if not provided
|
|
smbShareName := req.SMBShareName
|
|
if smbShareName == "" {
|
|
// Extract dataset name from full path (e.g., "pool/dataset" -> "dataset")
|
|
parts := strings.Split(datasetName, "/")
|
|
smbShareName = parts[len(parts)-1]
|
|
}
|
|
|
|
// Set SMB path (use mount_point if available, otherwise use dataset name)
|
|
smbPath := req.SMBPath
|
|
if smbPath == "" {
|
|
if mountPoint != "" && mountPoint != "none" {
|
|
smbPath = mountPoint
|
|
} else {
|
|
smbPath = fmt.Sprintf("/mnt/%s", strings.ReplaceAll(datasetName, "/", "_"))
|
|
}
|
|
}
|
|
|
|
// Insert into database
|
|
query := `
|
|
INSERT INTO zfs_shares (
|
|
dataset_id, share_type, nfs_enabled, nfs_options, nfs_clients,
|
|
smb_enabled, smb_share_name, smb_path, smb_comment,
|
|
smb_guest_ok, smb_read_only, smb_browseable, is_active, created_by
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
|
RETURNING id, created_at, updated_at
|
|
`
|
|
|
|
var shareID string
|
|
var createdAt, updatedAt time.Time
|
|
|
|
// Handle nfs_clients array - use empty array if nil
|
|
nfsClients := req.NFSClients
|
|
if nfsClients == nil {
|
|
nfsClients = []string{}
|
|
}
|
|
|
|
err = s.db.QueryRowContext(ctx, query,
|
|
datasetID, shareType, req.NFSEnabled, nfsOptions, pq.Array(nfsClients),
|
|
req.SMBEnabled, smbShareName, smbPath, req.SMBComment,
|
|
req.SMBGuestOK, req.SMBReadOnly, req.SMBBrowseable, true, userID,
|
|
).Scan(&shareID, &createdAt, &updatedAt)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create share: %w", err)
|
|
}
|
|
|
|
// Apply NFS export if enabled
|
|
if req.NFSEnabled {
|
|
if err := s.applyNFSExport(ctx, mountPoint, nfsOptions, req.NFSClients); err != nil {
|
|
s.logger.Error("Failed to apply NFS export", "error", err, "share_id", shareID)
|
|
// Don't fail the creation, but log the error
|
|
}
|
|
}
|
|
|
|
// Apply SMB share if enabled
|
|
if req.SMBEnabled {
|
|
if err := s.applySMBShare(ctx, smbShareName, smbPath, req.SMBComment, req.SMBGuestOK, req.SMBReadOnly, req.SMBBrowseable); err != nil {
|
|
s.logger.Error("Failed to apply SMB share", "error", err, "share_id", shareID)
|
|
// Don't fail the creation, but log the error
|
|
}
|
|
}
|
|
|
|
// Return the created share
|
|
return s.GetShare(ctx, shareID)
|
|
}
|
|
|
|
// UpdateShareRequest represents a share update request
|
|
type UpdateShareRequest struct {
|
|
NFSEnabled *bool `json:"nfs_enabled"`
|
|
NFSOptions *string `json:"nfs_options"`
|
|
NFSClients *[]string `json:"nfs_clients"`
|
|
SMBEnabled *bool `json:"smb_enabled"`
|
|
SMBShareName *string `json:"smb_share_name"`
|
|
SMBComment *string `json:"smb_comment"`
|
|
SMBGuestOK *bool `json:"smb_guest_ok"`
|
|
SMBReadOnly *bool `json:"smb_read_only"`
|
|
SMBBrowseable *bool `json:"smb_browseable"`
|
|
IsActive *bool `json:"is_active"`
|
|
}
|
|
|
|
// UpdateShare updates an existing share
|
|
func (s *Service) UpdateShare(ctx context.Context, shareID string, req *UpdateShareRequest) (*Share, error) {
|
|
// Get current share
|
|
share, err := s.GetShare(ctx, shareID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Build update query dynamically
|
|
updates := []string{}
|
|
args := []interface{}{}
|
|
argIndex := 1
|
|
|
|
if req.NFSEnabled != nil {
|
|
updates = append(updates, fmt.Sprintf("nfs_enabled = $%d", argIndex))
|
|
args = append(args, *req.NFSEnabled)
|
|
argIndex++
|
|
}
|
|
if req.NFSOptions != nil {
|
|
updates = append(updates, fmt.Sprintf("nfs_options = $%d", argIndex))
|
|
args = append(args, *req.NFSOptions)
|
|
argIndex++
|
|
}
|
|
if req.NFSClients != nil {
|
|
updates = append(updates, fmt.Sprintf("nfs_clients = $%d", argIndex))
|
|
args = append(args, pq.Array(*req.NFSClients))
|
|
argIndex++
|
|
}
|
|
if req.SMBEnabled != nil {
|
|
updates = append(updates, fmt.Sprintf("smb_enabled = $%d", argIndex))
|
|
args = append(args, *req.SMBEnabled)
|
|
argIndex++
|
|
}
|
|
if req.SMBShareName != nil {
|
|
updates = append(updates, fmt.Sprintf("smb_share_name = $%d", argIndex))
|
|
args = append(args, *req.SMBShareName)
|
|
argIndex++
|
|
}
|
|
if req.SMBComment != nil {
|
|
updates = append(updates, fmt.Sprintf("smb_comment = $%d", argIndex))
|
|
args = append(args, *req.SMBComment)
|
|
argIndex++
|
|
}
|
|
if req.SMBGuestOK != nil {
|
|
updates = append(updates, fmt.Sprintf("smb_guest_ok = $%d", argIndex))
|
|
args = append(args, *req.SMBGuestOK)
|
|
argIndex++
|
|
}
|
|
if req.SMBReadOnly != nil {
|
|
updates = append(updates, fmt.Sprintf("smb_read_only = $%d", argIndex))
|
|
args = append(args, *req.SMBReadOnly)
|
|
argIndex++
|
|
}
|
|
if req.SMBBrowseable != nil {
|
|
updates = append(updates, fmt.Sprintf("smb_browseable = $%d", argIndex))
|
|
args = append(args, *req.SMBBrowseable)
|
|
argIndex++
|
|
}
|
|
if req.IsActive != nil {
|
|
updates = append(updates, fmt.Sprintf("is_active = $%d", argIndex))
|
|
args = append(args, *req.IsActive)
|
|
argIndex++
|
|
}
|
|
|
|
if len(updates) == 0 {
|
|
return share, nil // No changes
|
|
}
|
|
|
|
// Update share_type based on enabled protocols
|
|
nfsEnabled := share.NFSEnabled
|
|
smbEnabled := share.SMBEnabled
|
|
if req.NFSEnabled != nil {
|
|
nfsEnabled = *req.NFSEnabled
|
|
}
|
|
if req.SMBEnabled != nil {
|
|
smbEnabled = *req.SMBEnabled
|
|
}
|
|
|
|
shareType := "none"
|
|
if nfsEnabled && smbEnabled {
|
|
shareType = "both"
|
|
} else if nfsEnabled {
|
|
shareType = "nfs"
|
|
} else if smbEnabled {
|
|
shareType = "smb"
|
|
}
|
|
|
|
updates = append(updates, fmt.Sprintf("share_type = $%d", argIndex))
|
|
args = append(args, shareType)
|
|
argIndex++
|
|
|
|
updates = append(updates, fmt.Sprintf("updated_at = NOW()"))
|
|
args = append(args, shareID)
|
|
|
|
query := fmt.Sprintf(`
|
|
UPDATE zfs_shares
|
|
SET %s
|
|
WHERE id = $%d
|
|
`, strings.Join(updates, ", "), argIndex)
|
|
|
|
_, err = s.db.ExecContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to update share: %w", err)
|
|
}
|
|
|
|
// Re-apply NFS export if NFS is enabled
|
|
if nfsEnabled {
|
|
nfsOptions := share.NFSOptions
|
|
if req.NFSOptions != nil {
|
|
nfsOptions = *req.NFSOptions
|
|
}
|
|
nfsClients := share.NFSClients
|
|
if req.NFSClients != nil {
|
|
nfsClients = *req.NFSClients
|
|
}
|
|
if err := s.applyNFSExport(ctx, share.MountPoint, nfsOptions, nfsClients); err != nil {
|
|
s.logger.Error("Failed to apply NFS export", "error", err, "share_id", shareID)
|
|
}
|
|
} else {
|
|
// Remove NFS export if disabled
|
|
if err := s.removeNFSExport(ctx, share.MountPoint); err != nil {
|
|
s.logger.Error("Failed to remove NFS export", "error", err, "share_id", shareID)
|
|
}
|
|
}
|
|
|
|
// Re-apply SMB share if SMB is enabled
|
|
if smbEnabled {
|
|
smbShareName := share.SMBShareName
|
|
if req.SMBShareName != nil {
|
|
smbShareName = *req.SMBShareName
|
|
}
|
|
smbPath := share.SMBPath
|
|
smbComment := share.SMBComment
|
|
if req.SMBComment != nil {
|
|
smbComment = *req.SMBComment
|
|
}
|
|
smbGuestOK := share.SMBGuestOK
|
|
if req.SMBGuestOK != nil {
|
|
smbGuestOK = *req.SMBGuestOK
|
|
}
|
|
smbReadOnly := share.SMBReadOnly
|
|
if req.SMBReadOnly != nil {
|
|
smbReadOnly = *req.SMBReadOnly
|
|
}
|
|
smbBrowseable := share.SMBBrowseable
|
|
if req.SMBBrowseable != nil {
|
|
smbBrowseable = *req.SMBBrowseable
|
|
}
|
|
if err := s.applySMBShare(ctx, smbShareName, smbPath, smbComment, smbGuestOK, smbReadOnly, smbBrowseable); err != nil {
|
|
s.logger.Error("Failed to apply SMB share", "error", err, "share_id", shareID)
|
|
}
|
|
} else {
|
|
// Remove SMB share if disabled
|
|
if err := s.removeSMBShare(ctx, share.SMBShareName); err != nil {
|
|
s.logger.Error("Failed to remove SMB share", "error", err, "share_id", shareID)
|
|
}
|
|
}
|
|
|
|
return s.GetShare(ctx, shareID)
|
|
}
|
|
|
|
// DeleteShare deletes a share
|
|
func (s *Service) DeleteShare(ctx context.Context, shareID string) error {
|
|
// Get share to get mount point and share name
|
|
share, err := s.GetShare(ctx, shareID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Remove NFS export
|
|
if share.NFSEnabled {
|
|
if err := s.removeNFSExport(ctx, share.MountPoint); err != nil {
|
|
s.logger.Error("Failed to remove NFS export", "error", err, "share_id", shareID)
|
|
}
|
|
}
|
|
|
|
// Remove SMB share
|
|
if share.SMBEnabled {
|
|
if err := s.removeSMBShare(ctx, share.SMBShareName); err != nil {
|
|
s.logger.Error("Failed to remove SMB share", "error", err, "share_id", shareID)
|
|
}
|
|
}
|
|
|
|
// Delete from database
|
|
_, err = s.db.ExecContext(ctx, "DELETE FROM zfs_shares WHERE id = $1", shareID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete share: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// applyNFSExport adds or updates an NFS export in /etc/exports
|
|
func (s *Service) applyNFSExport(ctx context.Context, mountPoint, options string, clients []string) error {
|
|
if mountPoint == "" || mountPoint == "none" {
|
|
return fmt.Errorf("mount point is required for NFS export")
|
|
}
|
|
|
|
// Build client list (default to * if empty)
|
|
clientList := "*"
|
|
if len(clients) > 0 {
|
|
clientList = strings.Join(clients, " ")
|
|
}
|
|
|
|
// Build export line
|
|
exportLine := fmt.Sprintf("%s %s(%s)", mountPoint, clientList, options)
|
|
|
|
// Read current /etc/exports
|
|
exportsPath := "/etc/exports"
|
|
exportsContent, err := os.ReadFile(exportsPath)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("failed to read exports file: %w", err)
|
|
}
|
|
|
|
lines := strings.Split(string(exportsContent), "\n")
|
|
var newLines []string
|
|
found := false
|
|
|
|
// Check if this mount point already exists
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
newLines = append(newLines, line)
|
|
continue
|
|
}
|
|
|
|
// Check if this line is for our mount point
|
|
if strings.HasPrefix(line, mountPoint+" ") {
|
|
newLines = append(newLines, exportLine)
|
|
found = true
|
|
} else {
|
|
newLines = append(newLines, line)
|
|
}
|
|
}
|
|
|
|
// Add if not found
|
|
if !found {
|
|
newLines = append(newLines, exportLine)
|
|
}
|
|
|
|
// Write back to file
|
|
newContent := strings.Join(newLines, "\n") + "\n"
|
|
if err := os.WriteFile(exportsPath, []byte(newContent), 0644); err != nil {
|
|
return fmt.Errorf("failed to write exports file: %w", err)
|
|
}
|
|
|
|
// Apply exports
|
|
cmd := exec.CommandContext(ctx, "sudo", "exportfs", "-ra")
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to apply exports: %s: %w", string(output), err)
|
|
}
|
|
|
|
s.logger.Info("NFS export applied", "mount_point", mountPoint, "clients", clientList)
|
|
return nil
|
|
}
|
|
|
|
// removeNFSExport removes an NFS export from /etc/exports
|
|
func (s *Service) removeNFSExport(ctx context.Context, mountPoint string) error {
|
|
if mountPoint == "" || mountPoint == "none" {
|
|
return nil // Nothing to remove
|
|
}
|
|
|
|
exportsPath := "/etc/exports"
|
|
exportsContent, err := os.ReadFile(exportsPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil // File doesn't exist, nothing to remove
|
|
}
|
|
return fmt.Errorf("failed to read exports file: %w", err)
|
|
}
|
|
|
|
lines := strings.Split(string(exportsContent), "\n")
|
|
var newLines []string
|
|
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
newLines = append(newLines, line)
|
|
continue
|
|
}
|
|
|
|
// Skip lines for this mount point
|
|
if strings.HasPrefix(line, mountPoint+" ") {
|
|
continue
|
|
}
|
|
|
|
newLines = append(newLines, line)
|
|
}
|
|
|
|
// Write back to file
|
|
newContent := strings.Join(newLines, "\n")
|
|
if newContent != "" && !strings.HasSuffix(newContent, "\n") {
|
|
newContent += "\n"
|
|
}
|
|
if err := os.WriteFile(exportsPath, []byte(newContent), 0644); err != nil {
|
|
return fmt.Errorf("failed to write exports file: %w", err)
|
|
}
|
|
|
|
// Apply exports
|
|
cmd := exec.CommandContext(ctx, "sudo", "exportfs", "-ra")
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to apply exports: %s: %w", string(output), err)
|
|
}
|
|
|
|
s.logger.Info("NFS export removed", "mount_point", mountPoint)
|
|
return nil
|
|
}
|
|
|
|
// applySMBShare adds or updates an SMB share in /etc/samba/smb.conf
|
|
func (s *Service) applySMBShare(ctx context.Context, shareName, path, comment string, guestOK, readOnly, browseable bool) error {
|
|
if shareName == "" {
|
|
return fmt.Errorf("SMB share name is required")
|
|
}
|
|
if path == "" {
|
|
return fmt.Errorf("SMB path is required")
|
|
}
|
|
|
|
smbConfPath := "/etc/samba/smb.conf"
|
|
smbContent, err := os.ReadFile(smbConfPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read smb.conf: %w", err)
|
|
}
|
|
|
|
// Parse and update smb.conf
|
|
lines := strings.Split(string(smbContent), "\n")
|
|
var newLines []string
|
|
inShare := false
|
|
shareStart := -1
|
|
|
|
for i, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
|
|
// Check if we're entering our share section
|
|
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
|
|
sectionName := trimmed[1 : len(trimmed)-1]
|
|
if sectionName == shareName {
|
|
inShare = true
|
|
shareStart = i
|
|
continue
|
|
} else if inShare {
|
|
// We've left our share section, insert the share config here
|
|
newLines = append(newLines, s.buildSMBShareConfig(shareName, path, comment, guestOK, readOnly, browseable))
|
|
inShare = false
|
|
}
|
|
}
|
|
|
|
if inShare {
|
|
// Skip lines until we find the next section or end of file
|
|
continue
|
|
}
|
|
|
|
newLines = append(newLines, line)
|
|
}
|
|
|
|
// If we were still in the share at the end, add it
|
|
if inShare {
|
|
newLines = append(newLines, s.buildSMBShareConfig(shareName, path, comment, guestOK, readOnly, browseable))
|
|
} else if shareStart == -1 {
|
|
// Share doesn't exist, add it at the end
|
|
newLines = append(newLines, "")
|
|
newLines = append(newLines, s.buildSMBShareConfig(shareName, path, comment, guestOK, readOnly, browseable))
|
|
}
|
|
|
|
// Write back to file
|
|
newContent := strings.Join(newLines, "\n")
|
|
if err := os.WriteFile(smbConfPath, []byte(newContent), 0644); err != nil {
|
|
return fmt.Errorf("failed to write smb.conf: %w", err)
|
|
}
|
|
|
|
// Reload Samba
|
|
cmd := exec.CommandContext(ctx, "sudo", "systemctl", "reload", "smbd")
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
// Try restart if reload fails
|
|
cmd = exec.CommandContext(ctx, "sudo", "systemctl", "restart", "smbd")
|
|
if output2, err2 := cmd.CombinedOutput(); err2 != nil {
|
|
return fmt.Errorf("failed to reload/restart smbd: %s / %s: %w", string(output), string(output2), err2)
|
|
}
|
|
}
|
|
|
|
s.logger.Info("SMB share applied", "share_name", shareName, "path", path)
|
|
return nil
|
|
}
|
|
|
|
// buildSMBShareConfig builds the SMB share configuration block
|
|
func (s *Service) buildSMBShareConfig(shareName, path, comment string, guestOK, readOnly, browseable bool) string {
|
|
var config []string
|
|
config = append(config, fmt.Sprintf("[%s]", shareName))
|
|
if comment != "" {
|
|
config = append(config, fmt.Sprintf(" comment = %s", comment))
|
|
}
|
|
config = append(config, fmt.Sprintf(" path = %s", path))
|
|
if guestOK {
|
|
config = append(config, " guest ok = yes")
|
|
} else {
|
|
config = append(config, " guest ok = no")
|
|
}
|
|
if readOnly {
|
|
config = append(config, " read only = yes")
|
|
} else {
|
|
config = append(config, " read only = no")
|
|
}
|
|
if browseable {
|
|
config = append(config, " browseable = yes")
|
|
} else {
|
|
config = append(config, " browseable = no")
|
|
}
|
|
return strings.Join(config, "\n")
|
|
}
|
|
|
|
// removeSMBShare removes an SMB share from /etc/samba/smb.conf
|
|
func (s *Service) removeSMBShare(ctx context.Context, shareName string) error {
|
|
if shareName == "" {
|
|
return nil // Nothing to remove
|
|
}
|
|
|
|
smbConfPath := "/etc/samba/smb.conf"
|
|
smbContent, err := os.ReadFile(smbConfPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil // File doesn't exist, nothing to remove
|
|
}
|
|
return fmt.Errorf("failed to read smb.conf: %w", err)
|
|
}
|
|
|
|
lines := strings.Split(string(smbContent), "\n")
|
|
var newLines []string
|
|
inShare := false
|
|
|
|
for _, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
|
|
// Check if we're entering our share section
|
|
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
|
|
sectionName := trimmed[1 : len(trimmed)-1]
|
|
if sectionName == shareName {
|
|
inShare = true
|
|
continue
|
|
} else if inShare {
|
|
// We've left our share section
|
|
inShare = false
|
|
}
|
|
}
|
|
|
|
if inShare {
|
|
// Skip lines in this share section
|
|
continue
|
|
}
|
|
|
|
newLines = append(newLines, line)
|
|
}
|
|
|
|
// Write back to file
|
|
newContent := strings.Join(newLines, "\n")
|
|
if err := os.WriteFile(smbConfPath, []byte(newContent), 0644); err != nil {
|
|
return fmt.Errorf("failed to write smb.conf: %w", err)
|
|
}
|
|
|
|
// Reload Samba
|
|
cmd := exec.CommandContext(ctx, "sudo", "systemctl", "reload", "smbd")
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
// Try restart if reload fails
|
|
cmd = exec.CommandContext(ctx, "sudo", "systemctl", "restart", "smbd")
|
|
if output2, err2 := cmd.CombinedOutput(); err2 != nil {
|
|
return fmt.Errorf("failed to reload/restart smbd: %s / %s: %w", string(output), string(output2), err2)
|
|
}
|
|
}
|
|
|
|
s.logger.Info("SMB share removed", "share_name", shareName)
|
|
return nil
|
|
}
|