feat: import existing smb.conf and /etc/exports on first boot

Adds auto-detection of pre-existing Samba shares and NFS exports when
nasctl is installed on a host that already has these configs.

New package internal/importer parses smb.conf (INI-style) and
/etc/exports (line-based) and imports them into SQLite.

Imported shares/exports are marked dirty so the user must review
and apply manually before any file is overwritten.

Backup: before the first Apply, each module backs up the original
config to <path>.nasctl.bak.<timestamp> (one time only).

New CLI flag --import-on-boot / NASCTL_IMPORT_ON_BOOT env var
(default false, opt-in).

New API endpoints:
  GET  /api/import/status
  POST /api/import/samba
  POST /api/import/nfs

New DB methods ReplaceSambaShares/ReplaceNFSExports (transactional
replace-all), guarded by import.samba.done / import.nfs.done
settings flags.
This commit is contained in:
2026-07-05 21:45:25 -04:00
parent d6b184d19b
commit 4ae7335b31
14 changed files with 1153 additions and 0 deletions
+14
View File
@@ -10,6 +10,7 @@ import (
"github.com/darroyo/nasctl/internal/db"
"github.com/darroyo/nasctl/internal/engine"
"github.com/darroyo/nasctl/internal/importer"
"github.com/darroyo/nasctl/internal/modules/nfs"
"github.com/darroyo/nasctl/internal/modules/samba"
"github.com/darroyo/nasctl/internal/modules/users"
@@ -25,6 +26,7 @@ func main() {
allowedRoots := flag.String("allowed-roots", envOrDefault("NASCTL_ALLOWED_ROOTS", ""), "Comma-separated allowed root dirs for shares/exports (empty = any absolute path)")
adminUser := flag.String("admin-user", envOrDefault("NASCTL_ADMIN_USER", "admin"), "Initial admin username (only used if no admin exists)")
adminPass := flag.String("admin-pass", envOrDefault("NASCTL_ADMIN_PASSWORD", "admin"), "Initial admin password (only used if no admin exists)")
importOnBoot := flag.Bool("import-on-boot", envOrDefault("NASCTL_IMPORT_ON_BOOT", "false") == "true", "Import existing smb.conf and /etc/exports on first boot")
flag.Parse()
if err := os.MkdirAll(filepath.Dir(*dbPath), 0o755); err != nil {
@@ -53,6 +55,16 @@ func main() {
log.Printf("created initial admin user %q — change the password after first login", *adminUser)
}
if *importOnBoot {
result := importer.ImportOnBoot(nil, database, *smbConfPath, *exportsPath)
if result.SambaImported > 0 {
log.Printf("[importer] imported %d existing samba shares — review and apply", result.SambaImported)
}
if result.NFSImported > 0 {
log.Printf("[importer] imported %d existing nfs exports — review and apply", result.NFSImported)
}
}
sambaModule := samba.New(samba.Config{
SMBConfPath: *smbConfPath,
Reload: *execSystem,
@@ -70,6 +82,8 @@ func main() {
srv := web.NewServer(database, eng, web.Options{
AllowedRoots: parseRoots(*allowedRoots),
Auth: auth,
SMBConfPath: *smbConfPath,
ExportsPath: *exportsPath,
})
log.Printf("nasctl listening on %s (db=%s exec-system=%v)", *addr, *dbPath, *execSystem)