diff --git a/cmd/incusd/api_1.0.go b/cmd/incusd/api_1.0.go index 841ccdcf5e3..6999fdf7a42 100644 --- a/cmd/incusd/api_1.0.go +++ b/cmd/incusd/api_1.0.go @@ -636,6 +636,13 @@ func doApi10Update(d *Daemon, r *http.Request, req api.ServerPut, patch bool) re } } + if nodeValues["storage.logs_volume"] != "" && nodeValues["storage.logs_volume"] != newNodeConfig.StorageLogsVolume() { + err := daemonStorageValidate(s, nodeValues["storage.logs_volume"]) + if err != nil { + return fmt.Errorf("Failed validation of %q: %w", "storage.logs_volume", err) + } + } + if patch { nodeChanged, err = newNodeConfig.Patch(nodeValues) } else { @@ -950,6 +957,14 @@ func doApi10UpdateTriggers(d *Daemon, nodeChanged, clusterChanged map[string]str } } + value, ok = nodeChanged["storage.logs_volume"] + if ok { + err := daemonStorageMove(s, "logs", value) + if err != nil { + return err + } + } + // Apply larger changes. if acmeChanged { err := autoRenewCertificate(s.ShutdownCtx, d, true) diff --git a/cmd/incusd/daemon_storage.go b/cmd/incusd/daemon_storage.go index f65286eec9d..38ef1f32a92 100644 --- a/cmd/incusd/daemon_storage.go +++ b/cmd/incusd/daemon_storage.go @@ -22,6 +22,7 @@ import ( func daemonStorageVolumesUnmount(s *state.State) error { var storageBackups string var storageImages string + var storageLogs string err := s.DB.Node.Transaction(context.Background(), func(ctx context.Context, tx *db.NodeTx) error { nodeConfig, err := node.ConfigLoad(ctx, tx) @@ -31,6 +32,7 @@ func daemonStorageVolumesUnmount(s *state.State) error { storageBackups = nodeConfig.StorageBackupsVolume() storageImages = nodeConfig.StorageImagesVolume() + storageLogs = nodeConfig.StorageLogsVolume() return nil }) @@ -73,12 +75,20 @@ func daemonStorageVolumesUnmount(s *state.State) error { } } + if storageLogs != "" { + err := unmount("logs", storageLogs) + if err != nil { + return fmt.Errorf("Failed to unmount logs storage: %w", err) + } + } + return nil } func daemonStorageMount(s *state.State) error { var storageBackups string var storageImages string + var storageLogs string err := s.DB.Node.Transaction(context.Background(), func(ctx context.Context, tx *db.NodeTx) error { nodeConfig, err := node.ConfigLoad(ctx, tx) if err != nil { @@ -87,6 +97,7 @@ func daemonStorageMount(s *state.State) error { storageBackups = nodeConfig.StorageBackupsVolume() storageImages = nodeConfig.StorageImagesVolume() + storageLogs = nodeConfig.StorageLogsVolume() return nil }) @@ -138,6 +149,13 @@ func daemonStorageMount(s *state.State) error { } } + if storageLogs != "" { + err := mount("logs", storageLogs) + if err != nil { + return fmt.Errorf("Failed to mount logs storage: %w", err) + } + } + return nil } @@ -241,6 +259,8 @@ func daemonStorageValidate(s *state.State, target string) error { } func daemonStorageMove(s *state.State, storageType string, target string) error { + isLogs := storageType == "logs" + sysLogDir := internalUtil.LogPath() destPath := internalUtil.VarPath(storageType) // Track down the current storage. @@ -279,6 +299,33 @@ func daemonStorageMove(s *state.State, storageType string, target string) error return nil } + moveInstanceDirs := func(source string, target string) error { + entries, err := os.ReadDir(source) + if err != nil { + return err + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + src := filepath.Join(source, entry.Name()) + dst := filepath.Join(target, entry.Name()) + _, err := rsync.LocalCopy(src, dst, "", false) + if err != nil { + return err + } + + err = os.RemoveAll(src) + if err != nil { + return err + } + } + + return nil + } + // Deal with unsetting. if target == "" { // Things already look correct. @@ -292,16 +339,29 @@ func daemonStorageMove(s *state.State, storageType string, target string) error return fmt.Errorf("Failed to delete storage symlink at %q: %w", destPath, err) } - // Re-create as a directory. - err = os.MkdirAll(destPath, 0o700) - if err != nil { - return fmt.Errorf("Failed to create directory %q: %w", destPath, err) - } + if isLogs { + // Ensure system log dir exists and move instance dirs back there. + err = os.MkdirAll(sysLogDir, 0o700) + if err != nil { + return fmt.Errorf("Failed to create system log directory %q: %w", sysLogDir, err) + } - // Move the data across. - err = moveContent(sourcePath, destPath) - if err != nil { - return fmt.Errorf("Failed to move data over to directory %q: %w", destPath, err) + err = moveInstanceDirs(sourcePath, sysLogDir) + if err != nil { + return fmt.Errorf("Failed to move instance logs back to %q: %w", sysLogDir, err) + } + } else { + // Re-create as a directory. + err = os.MkdirAll(destPath, 0o700) + if err != nil { + return fmt.Errorf("Failed to create directory %q: %w", destPath, err) + } + + // Move the data across. + err = moveContent(sourcePath, destPath) + if err != nil { + return fmt.Errorf("Failed to move data over to directory %q: %w", destPath, err) + } } pool, err := storagePools.LoadByName(s, sourcePool) @@ -366,7 +426,11 @@ func daemonStorageMove(s *state.State, storageType string, target string) error } // Move the data across. - err = moveContent(sourcePath, destPath) + if isLogs { + err = moveInstanceDirs(sourcePath, destPath) + } else { + err = moveContent(sourcePath, destPath) + } if err != nil { return fmt.Errorf("Failed to move data over to directory %q: %w", destPath, err) } @@ -401,7 +465,11 @@ func daemonStorageMove(s *state.State, storageType string, target string) error } // Move the data across. - err = moveContent(sourcePath, destPath) + if isLogs { + err = moveInstanceDirs(sysLogDir, destPath) + } else { + err = moveContent(sourcePath, destPath) + } if err != nil { return fmt.Errorf("Failed to move data over to directory %q: %w", destPath, err) } diff --git a/internal/server/metadata/configuration.json b/internal/server/metadata/configuration.json index f634e3dff76..289945ab19c 100644 --- a/internal/server/metadata/configuration.json +++ b/internal/server/metadata/configuration.json @@ -5727,6 +5727,14 @@ "type": "string" } }, + { + "storage.logs_volume": { + "longdesc": "Specify the volume using the syntax `POOL/VOLUME`.", + "scope": "local", + "shortdesc": "Volume to use to store instance log directories", + "type": "string" + } + }, { "storage.linstor.ca_cert": { "longdesc": "", diff --git a/internal/server/node/config.go b/internal/server/node/config.go index 073ddda099c..f90c4d1be2d 100644 --- a/internal/server/node/config.go +++ b/internal/server/node/config.go @@ -116,6 +116,11 @@ func (c *Config) StorageImagesVolume() string { return c.m.GetString("storage.images_volume") } +// StorageLogsVolume returns the name of the pool/volume to use for storing instance log directories. +func (c *Config) StorageLogsVolume() string { + return c.m.GetString("storage.logs_volume") +} + // LinstorSatelliteName returns the LINSTOR satellite name override. func (c *Config) LinstorSatelliteName() string { return c.m.GetString("storage.linstor.satellite.name") @@ -261,7 +266,7 @@ var ConfigSchema = config.Schema{ // shortdesc: OVS socket path "network.ovs.connection": {Default: "unix:/run/openvswitch/db.sock"}, - // Storage volumes to store backups/images on + // Storage volumes to store backups/images/logs on // gendoc:generate(entity=server, group=miscellaneous, key=storage.backups_volume) // Specify the volume using the syntax `POOL/VOLUME`. @@ -279,6 +284,14 @@ var ConfigSchema = config.Schema{ // shortdesc: Volume to use to store the image tarballs "storage.images_volume": {}, + // gendoc:generate(entity=server, group=miscellaneous, key=storage.logs_volume) + // Specify the volume using the syntax `POOL/VOLUME`. + // --- + // type: string + // scope: local + // shortdesc: Volume to use to store instance log directories + "storage.logs_volume": {}, + // LINSTOR // gendoc:generate(entity=server, group=miscellaneous, key=storage.linstor.satellite.name)