package main
import (
"context"
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"slices"
"strings"
"time"
"github.com/lxc/incus/v6/internal/revert"
"github.com/lxc/incus/v6/internal/server/backup"
"github.com/lxc/incus/v6/internal/server/certificate"
"github.com/lxc/incus/v6/internal/server/cluster"
"github.com/lxc/incus/v6/internal/server/db"
dbCluster "github.com/lxc/incus/v6/internal/server/db/cluster"
"github.com/lxc/incus/v6/internal/server/db/query"
"github.com/lxc/incus/v6/internal/server/instance"
"github.com/lxc/incus/v6/internal/server/instance/instancetype"
"github.com/lxc/incus/v6/internal/server/network"
"github.com/lxc/incus/v6/internal/server/node"
"github.com/lxc/incus/v6/internal/server/project"
storagePools "github.com/lxc/incus/v6/internal/server/storage"
storageDrivers "github.com/lxc/incus/v6/internal/server/storage/drivers"
internalUtil "github.com/lxc/incus/v6/internal/util"
"github.com/lxc/incus/v6/shared/api"
"github.com/lxc/incus/v6/shared/logger"
"github.com/lxc/incus/v6/shared/subprocess"
"github.com/lxc/incus/v6/shared/util"
)
type patchStage int
const (
patchNoStageSet patchStage = iota
patchPreDaemonStorage
patchPostDaemonStorage
patchPostNetworks
)
Patches are one-time actions that are sometimes needed to update
existing container configuration or move things around on the
filesystem.
Those patches are applied at startup time after the database schema
has been fully updated. Patches can therefore assume a working database.
At the time the patches are applied, the containers aren't started
yet and the daemon isn't listening to requests.
DO NOT use this mechanism for database update. Schema updates must be
done through the separate schema update mechanism.
Only append to the patches list, never remove entries and never re-order them.
*/
var patches = []patch{
{name: "storage_lvm_skipactivation", stage: patchPostDaemonStorage, run: patchGenericStorage},
{name: "clustering_drop_database_role", stage: patchPostDaemonStorage, run: patchClusteringDropDatabaseRole},
{name: "network_clear_bridge_volatile_hwaddr", stage: patchPostDaemonStorage, run: patchGenericNetwork(patchNetworkClearBridgeVolatileHwaddr)},
{name: "move_backups_instances", stage: patchPostDaemonStorage, run: patchMoveBackupsInstances},
{name: "network_ovn_enable_nat", stage: patchPostDaemonStorage, run: patchGenericNetwork(patchNetworkOVNEnableNAT)},
{name: "network_ovn_remove_routes", stage: patchPostDaemonStorage, run: patchGenericNetwork(patchNetworkOVNRemoveRoutes)},
{name: "thinpool_typo_fix", stage: patchPostDaemonStorage, run: patchThinpoolTypoFix},
{name: "vm_rename_uuid_key", stage: patchPostDaemonStorage, run: patchVMRenameUUIDKey},
{name: "db_nodes_autoinc", stage: patchPreDaemonStorage, run: patchDBNodesAutoInc},
{name: "network_acl_remove_defaults", stage: patchPostDaemonStorage, run: patchGenericNetwork(patchNetworkACLRemoveDefaults)},
{name: "clustering_server_cert_trust", stage: patchPreDaemonStorage, run: patchClusteringServerCertTrust},
{name: "warnings_remove_empty_node", stage: patchPostDaemonStorage, run: patchRemoveWarningsWithEmptyNode},
{name: "dnsmasq_entries_include_device_name", stage: patchPostDaemonStorage, run: patchDnsmasqEntriesIncludeDeviceName},
{name: "storage_missing_snapshot_records", stage: patchPostDaemonStorage, run: patchGenericStorage},
{name: "storage_delete_old_snapshot_records", stage: patchPostDaemonStorage, run: patchGenericStorage},
{name: "storage_zfs_drop_block_volume_filesystem_extension", stage: patchPostDaemonStorage, run: patchGenericStorage},
{name: "storage_prefix_bucket_names_with_project", stage: patchPostDaemonStorage, run: patchGenericStorage},
{name: "storage_move_custom_iso_block_volumes", stage: patchPostDaemonStorage, run: patchStorageRenameCustomISOBlockVolumes},
{name: "zfs_set_content_type_user_property", stage: patchPostDaemonStorage, run: patchZfsSetContentTypeUserProperty},
{name: "snapshots_rename", stage: patchPreDaemonStorage, run: patchSnapshotsRename},
{name: "storage_zfs_unset_invalid_block_settings", stage: patchPostDaemonStorage, run: patchStorageZfsUnsetInvalidBlockSettings},
{name: "storage_zfs_unset_invalid_block_settings_v2", stage: patchPostDaemonStorage, run: patchStorageZfsUnsetInvalidBlockSettingsV2},
{name: "runtime_directory", stage: patchPostDaemonStorage, run: patchRuntimeDirectory},
{name: "lvm_node_force_reuse", stage: patchPostDaemonStorage, run: patchLvmForceReuseKey},
}
type patch struct {
name string
stage patchStage
run func(name string, d *Daemon) error
}
func (p *patch) apply(d *Daemon) error {
logger.Info("Applying patch", logger.Ctx{"name": p.name})
err := p.run(p.name, d)
if err != nil {
return fmt.Errorf("Failed applying patch %q: %w", p.name, err)
}
err = d.db.Node.MarkPatchAsApplied(p.name)
if err != nil {
return fmt.Errorf("Failed marking patch applied %q: %w", p.name, err)
}
return nil
}
func patchesGetNames() []string {
names := make([]string, len(patches))
for i, patch := range patches {
if patch.stage == patchNoStageSet {
continue
}
names[i] = patch.name
}
return names
}
func patchesApply(d *Daemon, stage patchStage) error {
appliedPatches, err := d.db.Node.GetAppliedPatches()
if err != nil {
return err
}
for _, patch := range patches {
if patch.stage == patchNoStageSet {
return fmt.Errorf("Patch %q has no stage set: %d", patch.name, patch.stage)
}
if slices.Contains(appliedPatches, patch.name) {
continue
}
err := patch.apply(d)
if err != nil {
return err
}
}
return nil
}
func patchDnsmasqEntriesIncludeDeviceName(name string, d *Daemon) error {
err := network.UpdateDNSMasqStatic(d.State(), "")
if err != nil {
return err
}
return nil
}
func patchRemoveWarningsWithEmptyNode(name string, d *Daemon) error {
err := d.db.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
warnings, err := dbCluster.GetWarnings(ctx, tx.Tx())
if err != nil {
return err
}
for _, w := range warnings {
if w.Node == "" {
err = dbCluster.DeleteWarning(ctx, tx.Tx(), w.UUID)
if err != nil {
return err
}
}
}
return nil
})
if err != nil {
return err
}
return nil
}
func patchClusteringServerCertTrust(name string, d *Daemon) error {
if !d.serverClustered {
return nil
}
var serverName string
err := d.db.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
var err error
serverName, err = tx.GetLocalNodeName(ctx)
return err
})
if err != nil {
return err
}
serverCert, err := internalUtil.LoadServerCert(d.os.VarDir)
if err != nil {
return err
}
logger.Infof("Adding local server certificate to global trust store for %q patch", name)
err = d.db.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
return cluster.EnsureServerCertificateTrusted(serverName, serverCert, tx)
})
if err != nil {
return err
}
logger.Infof("Added local server certificate to global trust store for %q patch", name)
for {
var err error
var dbCerts []dbCluster.Certificate
err = d.db.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
dbCerts, err = dbCluster.GetCertificates(ctx, tx.Tx())
return err
})
if err != nil {
return err
}
trustedServerCerts := make(map[string]*dbCluster.Certificate)
for _, c := range dbCerts {
if c.Type == certificate.TypeServer {
trustedServerCerts[c.Name] = &c
}
}
var members []db.NodeInfo
err = d.db.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
members, err = tx.GetNodes(ctx)
if err != nil {
return fmt.Errorf("Failed getting cluster members: %w", err)
}
return nil
})
if err != nil {
return err
}
missingCerts := false
for _, member := range members {
_, found := trustedServerCerts[member.Name]
if !found {
logger.Warnf("Missing trusted server certificate for cluster member %q", member.Name)
missingCerts = true
break
}
}
if missingCerts {
logger.Warnf("Waiting for %q patch to be applied on all cluster members", name)
time.Sleep(time.Second)
continue
}
logger.Infof("Trusted server certificates found in trust store for all cluster members")
break
}
logger.Infof("Set client certificate to server certificate %v", serverCert.Fingerprint())
d.serverCertInt = serverCert
updateCertificateCache(d)
return nil
}
func patchNetworkACLRemoveDefaults(name string, d *Daemon) error {
var err error
var projectNames []string
err = d.db.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
projectNames, err = dbCluster.GetProjectNames(ctx, tx.Tx())
return err
})
if err != nil {
return err
}
err = d.db.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
for _, projectName := range projectNames {
aclNames, err := tx.GetNetworkACLs(ctx, projectName)
if err != nil {
return err
}
for _, aclName := range aclNames {
aclID, acl, err := tx.GetNetworkACL(ctx, projectName, aclName)
if err != nil {
return err
}
modified := false
_, found := acl.Config["default.action"]
if found {
delete(acl.Config, "default.action")
modified = true
}
_, found = acl.Config["default.logged"]
if found {
delete(acl.Config, "default.logged")
modified = true
}
if modified {
err = tx.UpdateNetworkACL(ctx, aclID, &acl.NetworkACLPut)
if err != nil {
return fmt.Errorf("Failed updating network ACL %d: %w", aclID, err)
}
}
}
}
return nil
})
if err != nil {
return err
}
return nil
}
func patchDBNodesAutoInc(name string, d *Daemon) error {
s := d.State()
for {
var schemaSQL string
row := s.DB.Cluster.DB().QueryRow("SELECT sql FROM sqlite_master WHERE name = 'nodes'")
err := row.Scan(&schemaSQL)
if err != nil {
return err
}
if strings.Contains(schemaSQL, "id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL") {
logger.Debugf(`Skipping %q patch as "nodes" table id column already AUTOINCREMENT`, name)
return nil
}
var localConfig *node.Config
err = d.db.Node.Transaction(context.TODO(), func(ctx context.Context, tx *db.NodeTx) error {
localConfig, err = node.ConfigLoad(ctx, tx)
return err
})
if err != nil {
return err
}
leaderAddress, err := s.Cluster.LeaderAddress()
if err != nil {
if errors.Is(err, cluster.ErrNodeIsNotClustered) {
break
}
return err
}
if localConfig.ClusterAddress() == leaderAddress {
break
}
logger.Warnf("Waiting for %q patch to be applied on leader cluster member", name)
time.Sleep(time.Second)
}
_, err := s.DB.Cluster.DB().Exec(`
PRAGMA foreign_keys=OFF; -- So that integrity doesn't get in the way for now.
PRAGMA legacy_alter_table = ON; -- So that views referencing this table don't block change.
CREATE TABLE nodes_new (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT NOT NULL,
description TEXT DEFAULT '',
address TEXT NOT NULL,
schema INTEGER NOT NULL,
api_extensions INTEGER NOT NULL,
heartbeat DATETIME DEFAULT CURRENT_TIMESTAMP,
state INTEGER NOT NULL DEFAULT 0,
arch INTEGER NOT NULL DEFAULT 0 CHECK (arch > 0),
failure_domain_id INTEGER DEFAULT NULL REFERENCES nodes_failure_domains (id) ON DELETE SET NULL,
UNIQUE (name),
UNIQUE (address)
);
INSERT INTO nodes_new (id, name, description, address, schema, api_extensions, heartbeat, state, arch, failure_domain_id)
SELECT id, name, description, address, schema, api_extensions, heartbeat, state, arch, failure_domain_id FROM nodes;
DROP TABLE nodes;
ALTER TABLE nodes_new RENAME TO nodes;
PRAGMA foreign_keys=ON; -- Make sure we turn integrity checks back on.
PRAGMA legacy_alter_table = OFF; -- So views check integrity again.
`)
return err
}
func patchVMRenameUUIDKey(name string, d *Daemon) error {
oldUUIDKey := "volatile.vm.uuid"
newUUIDKey := "volatile.uuid"
s := d.State()
return s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
return tx.InstanceList(ctx, func(inst db.InstanceArgs, p api.Project) error {
if inst.Type != instancetype.VM {
return nil
}
uuid := inst.Config[oldUUIDKey]
if uuid != "" {
changes := map[string]string{
oldUUIDKey: "",
newUUIDKey: uuid,
}
logger.Debugf("Renaming config key %q to %q for VM %q (Project %q)", oldUUIDKey, newUUIDKey, inst.Name, inst.Project)
err := tx.UpdateInstanceConfig(inst.ID, changes)
if err != nil {
return fmt.Errorf("Failed renaming config key %q to %q for VM %q (Project %q): %w", oldUUIDKey, newUUIDKey, inst.Name, inst.Project, err)
}
}
snaps, err := tx.GetInstanceSnapshotsWithName(ctx, inst.Project, inst.Name)
if err != nil {
return err
}
for _, snap := range snaps {
config, err := dbCluster.GetInstanceConfig(ctx, tx.Tx(), snap.ID)
if err != nil {
return err
}
uuid := config[oldUUIDKey]
if uuid != "" {
changes := map[string]string{
oldUUIDKey: "",
newUUIDKey: uuid,
}
logger.Debugf("Renaming config key %q to %q for VM %q (Project %q)", oldUUIDKey, newUUIDKey, snap.Name, snap.Project)
err = tx.UpdateInstanceSnapshotConfig(snap.ID, changes)
if err != nil {
return fmt.Errorf("Failed renaming config key %q to %q for VM %q (Project %q): %w", oldUUIDKey, newUUIDKey, snap.Name, snap.Project, err)
}
}
}
return nil
})
})
}
func patchThinpoolTypoFix(name string, d *Daemon) error {
revert := revert.New()
defer revert.Fail()
tx, err := d.db.Cluster.Begin()
if err != nil {
return fmt.Errorf("Failed to begin transaction: %w", err)
}
revert.Add(func() { _ = tx.Rollback() })
nodeIDs, err := query.SelectIntegers(context.TODO(), tx, "SELECT id FROM nodes")
if err != nil {
return fmt.Errorf("Failed to get IDs of current nodes: %w", err)
}
poolIDs, err := query.SelectIntegers(context.TODO(), tx, "SELECT id FROM storage_pools WHERE driver='lvm'")
if err != nil {
return fmt.Errorf("Failed to get IDs of current lvm pools: %w", err)
}
for _, poolID := range poolIDs {
config, err := query.SelectConfig(context.TODO(), tx, "storage_pools_config", "storage_pool_id=? AND node_id IS NULL", poolID)
if err != nil {
return fmt.Errorf("Failed to fetch of lvm pool config: %w", err)
}
value, ok := config["lvm.thinpool_name"]
if !ok {
continue
}
_, err = tx.Exec(`
DELETE FROM storage_pools_config WHERE key='lvm.thinpool_name' AND storage_pool_id=? AND node_id IS NULL
`, poolID)
if err != nil {
return fmt.Errorf("Failed to delete lvm.thinpool_name config: %w", err)
}
for _, nodeID := range nodeIDs {
_, err := tx.Exec(`
INSERT INTO storage_pools_config(storage_pool_id, node_id, key, value)
VALUES(?, ?, 'lvm.thinpool_name', ?)
`, poolID, nodeID, value)
if err != nil {
return fmt.Errorf("Failed to create lvm.thinpool_name node config: %w", err)
}
}
}
err = tx.Commit()
if err != nil {
return fmt.Errorf("Failed to commit transaction: %w", err)
}
revert.Success()
return nil
}
func patchNetworkOVNRemoveRoutes(name string, d *Daemon) error {
err := d.db.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
projectNetworks, err := tx.GetCreatedNetworks(ctx)
if err != nil {
return err
}
for projectName, networks := range projectNetworks {
for networkID, network := range networks {
if network.Type != "ovn" {
continue
}
modified := false
_, found := network.Config["ipv4.routes.external"]
if found {
modified = true
delete(network.Config, "ipv4.routes.external")
}
_, found = network.Config["ipv6.routes.external"]
if found {
modified = true
delete(network.Config, "ipv6.routes.external")
}
if modified {
err = tx.UpdateNetwork(ctx, projectName, network.Name, network.Description, network.Config)
if err != nil {
return fmt.Errorf("Failed removing OVN external route settings for %q (%d): %w", network.Name, networkID, err)
}
logger.Debugf("Removing external route settings for OVN network %q (%d)", network.Name, networkID)
}
}
}
return nil
})
if err != nil {
return err
}
return nil
}
func patchNetworkOVNEnableNAT(name string, d *Daemon) error {
err := d.db.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
projectNetworks, err := tx.GetCreatedNetworks(ctx)
if err != nil {
return err
}
for projectName, networks := range projectNetworks {
for networkID, network := range networks {
if network.Type != "ovn" {
continue
}
modified := false
if network.Config["ipv4.address"] != "" && network.Config["ipv4.nat"] == "" {
modified = true
network.Config["ipv4.nat"] = "true"
}
if network.Config["ipv6.address"] != "" && network.Config["ipv6.nat"] == "" {
modified = true
network.Config["ipv6.nat"] = "true"
}
if modified {
err = tx.UpdateNetwork(ctx, projectName, network.Name, network.Description, network.Config)
if err != nil {
return fmt.Errorf("Failed saving OVN NAT settings for %q (%d): %w", network.Name, networkID, err)
}
logger.Debugf("Enabling NAT for OVN network %q (%d)", network.Name, networkID)
}
}
}
return nil
})
if err != nil {
return err
}
return nil
}
func patchMoveBackupsInstances(name string, d *Daemon) error {
if !util.PathExists(internalUtil.VarPath("backups")) {
return nil
}
backupsPath := internalUtil.VarPath("backups", "instances")
err := os.MkdirAll(backupsPath, 0700)
if err != nil {
return fmt.Errorf("Failed creating instances backup directory %q: %w", backupsPath, err)
}
backups, err := os.ReadDir(internalUtil.VarPath("backups"))
if err != nil {
return fmt.Errorf("Failed listing existing backup directory %q: %w", internalUtil.VarPath("backups"), err)
}
for _, backupDir := range backups {
if backupDir.Name() == "instances" || strings.HasPrefix(backupDir.Name(), backup.WorkingDirPrefix) {
continue
}
oldPath := internalUtil.VarPath("backups", backupDir.Name())
newPath := filepath.Join(backupsPath, backupDir.Name())
logger.Debugf("Moving backup from %q to %q", oldPath, newPath)
err = os.Rename(oldPath, newPath)
if err != nil {
return fmt.Errorf("Failed moving backup from %q to %q: %w", oldPath, newPath, err)
}
}
return nil
}
func patchGenericStorage(name string, d *Daemon) error {
return storagePools.Patch(d.State(), name)
}
func patchGenericNetwork(f func(name string, d *Daemon) error) func(name string, d *Daemon) error {
return func(name string, d *Daemon) error {
err := network.PatchPreCheck()
if err != nil {
return err
}
return f(name, d)
}
}
func patchClusteringDropDatabaseRole(name string, d *Daemon) error {
return d.State().DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
members, err := tx.GetNodes(ctx)
if err != nil {
return fmt.Errorf("Failed getting cluster members: %w", err)
}
for _, member := range members {
err := tx.UpdateNodeRoles(member.ID, nil)
if err != nil {
return err
}
}
return nil
})
}
func patchNetworkClearBridgeVolatileHwaddr(name string, d *Daemon) error {
projectName := api.ProjectDefaultName
err := d.db.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
networks, err := tx.GetNetworks(ctx, projectName)
if err != nil {
return fmt.Errorf("Failed loading networks for network_clear_bridge_volatile_hwaddr patch: %w", err)
}
for _, networkName := range networks {
_, net, _, err := tx.GetNetworkInAnyState(ctx, projectName, networkName)
if err != nil {
return fmt.Errorf("Failed loading network %q for network_clear_bridge_volatile_hwaddr patch: %w", networkName, err)
}
if net.Config["volatile.bridge.hwaddr"] != "" {
delete(net.Config, "volatile.bridge.hwaddr")
err = tx.UpdateNetwork(ctx, projectName, net.Name, net.Description, net.Config)
if err != nil {
return fmt.Errorf("Failed updating network %q for network_clear_bridge_volatile_hwaddr patch: %w", networkName, err)
}
}
}
return nil
})
if err != nil {
return err
}
return nil
}
func patchStorageRenameCustomISOBlockVolumes(name string, d *Daemon) error {
s := d.State()
var pools []string
err := s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
var err error
pools, err = tx.GetStoragePoolNames(ctx)
return err
})
if err != nil {
if api.StatusErrorCheck(err, http.StatusNotFound) {
return nil
}
return fmt.Errorf("Failed getting storage pool names: %w", err)
}
var localConfig *node.Config
isLeader := false
err = d.db.Node.Transaction(s.ShutdownCtx, func(ctx context.Context, tx *db.NodeTx) error {
localConfig, err = node.ConfigLoad(ctx, tx)
return err
})
if err != nil {
return err
}
leaderAddress, err := s.Cluster.LeaderAddress()
if err != nil {
if errors.Is(err, cluster.ErrNodeIsNotClustered) {
isLeader = true
} else {
return err
}
} else if localConfig.ClusterAddress() == leaderAddress {
isLeader = true
}
volTypeCustom := db.StoragePoolVolumeTypeCustom
customPoolVolumes := make(map[string][]*db.StorageVolume, 0)
err = s.DB.Cluster.Transaction(s.ShutdownCtx, func(ctx context.Context, tx *db.ClusterTx) error {
for _, pool := range pools {
poolID, err := tx.GetStoragePoolID(ctx, pool)
if err != nil {
return fmt.Errorf("Failed getting storage pool ID of pool %q: %w", pool, err)
}
customVolumes, err := tx.GetStoragePoolVolumes(ctx, poolID, false, db.StorageVolumeFilter{Type: &volTypeCustom})
if err != nil {
return fmt.Errorf("Failed getting custom storage volumes of pool %q: %w", pool, err)
}
if customPoolVolumes[pool] == nil {
customPoolVolumes[pool] = []*db.StorageVolume{}
}
customPoolVolumes[pool] = append(customPoolVolumes[pool], customVolumes...)
}
return nil
})
if err != nil {
return err
}
for poolName, volumes := range customPoolVolumes {
p, err := storagePools.LoadByName(s, poolName)
if err != nil {
return fmt.Errorf("Failed loading pool %q: %w", poolName, err)
}
if p.Driver().Info().Remote && !isLeader {
continue
}
for _, vol := range volumes {
if s.ServerName != "" && vol.Location != s.ServerName {
continue
}
if vol.ContentType != db.StoragePoolVolumeContentTypeNameISO {
continue
}
oldVol := storageDrivers.NewVolume(p.Driver(), p.Name(), storageDrivers.VolumeTypeCustom, storageDrivers.ContentTypeBlock, project.StorageVolume(vol.Project, vol.Name), nil, nil)
err = p.Driver().RenameVolume(oldVol, fmt.Sprintf("%s.iso", oldVol.Name()), nil)
if err != nil {
return fmt.Errorf("Failed renaming volume: %w", err)
}
}
}
return nil
}
func patchZfsSetContentTypeUserProperty(name string, d *Daemon) error {
s := d.State()
var pools []string
err := s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
var err error
pools, err = tx.GetStoragePoolNames(ctx)
return err
})
if err != nil {
if api.StatusErrorCheck(err, http.StatusNotFound) {
return nil
}
return fmt.Errorf("Failed getting storage pool names: %w", err)
}
volTypeCustom := db.StoragePoolVolumeTypeCustom
customPoolVolumes := make(map[string][]*db.StorageVolume, 0)
err = s.DB.Cluster.Transaction(s.ShutdownCtx, func(ctx context.Context, tx *db.ClusterTx) error {
for _, pool := range pools {
poolID, err := tx.GetStoragePoolID(ctx, pool)
if err != nil {
return fmt.Errorf("Failed getting storage pool ID of pool %q: %w", pool, err)
}
customVolumes, err := tx.GetStoragePoolVolumes(ctx, poolID, false, db.StorageVolumeFilter{Type: &volTypeCustom})
if err != nil {
return fmt.Errorf("Failed getting custom storage volumes of pool %q: %w", pool, err)
}
if customPoolVolumes[pool] == nil {
customPoolVolumes[pool] = []*db.StorageVolume{}
}
customPoolVolumes[pool] = append(customPoolVolumes[pool], customVolumes...)
}
return nil
})
if err != nil {
return err
}
for poolName, volumes := range customPoolVolumes {
p, err := storagePools.LoadByName(s, poolName)
if err != nil {
return fmt.Errorf("Failed loading pool %q: %w", poolName, err)
}
if p.Driver().Info().Name != "zfs" {
continue
}
for _, vol := range volumes {
if s.ServerName != "" && vol.Location != s.ServerName {
continue
}
zfsPoolName := p.Driver().Config()["zfs.pool_name"]
if zfsPoolName != "" {
poolName = zfsPoolName
}
zfsVolName := fmt.Sprintf("%s/%s/%s", poolName, storageDrivers.VolumeTypeCustom, project.StorageVolume(vol.Project, vol.Name))
_, err = subprocess.RunCommand("zfs", "set", fmt.Sprintf("incus:content_type=%s", vol.ContentType), zfsVolName)
if err != nil {
logger.Debug("Failed setting incus:content_type property", logger.Ctx{"name": zfsVolName, "err": err})
}
}
}
return nil
}
func patchSnapshotsRename(name string, d *Daemon) error {
os.Remove(internalUtil.VarPath("containers-snapshots"))
return os.Rename(internalUtil.VarPath("snapshots"), internalUtil.VarPath("containers-snapshots"))
}
func patchStorageZfsUnsetInvalidBlockSettings(_ string, d *Daemon) error {
s := d.State()
var pools []string
err := s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
var err error
pools, err = tx.GetStoragePoolNames(ctx)
return err
})
if err != nil {
if api.StatusErrorCheck(err, http.StatusNotFound) {
return nil
}
return fmt.Errorf("Failed getting storage pool names: %w", err)
}
volTypeCustom := db.StoragePoolVolumeTypeCustom
volTypeVM := db.StoragePoolVolumeTypeVM
poolIDNameMap := make(map[int64]string, 0)
poolVolumes := make(map[int64][]*db.StorageVolume, 0)
err = s.DB.Cluster.Transaction(s.ShutdownCtx, func(ctx context.Context, tx *db.ClusterTx) error {
for _, pool := range pools {
poolID, err := tx.GetStoragePoolID(ctx, pool)
if err != nil {
return fmt.Errorf("Failed getting storage pool ID of pool %q: %w", pool, err)
}
driverName, err := tx.GetStoragePoolDriver(ctx, poolID)
if err != nil {
return fmt.Errorf("Failed getting storage pool driver of pool %q: %w", pool, err)
}
if driverName != "zfs" {
continue
}
volumes, err := tx.GetStoragePoolVolumes(ctx, poolID, false, db.StorageVolumeFilter{Type: &volTypeCustom}, db.StorageVolumeFilter{Type: &volTypeVM})
if err != nil {
return fmt.Errorf("Failed getting custom storage volumes of pool %q: %w", pool, err)
}
if poolVolumes[poolID] == nil {
poolVolumes[poolID] = []*db.StorageVolume{}
}
poolIDNameMap[poolID] = pool
poolVolumes[poolID] = append(poolVolumes[poolID], volumes...)
}
return nil
})
if err != nil {
return err
}
var volType int
for pool, volumes := range poolVolumes {
for _, vol := range volumes {
if s.ServerName != "" && vol.Location != s.ServerName {
continue
}
config := vol.Config
if util.IsTrue(config["zfs.block_mode"]) {
continue
}
update := false
for _, k := range []string{"block.filesystem", "block.mount_options"} {
_, found := config[k]
if found {
delete(config, k)
update = true
}
}
if !update {
continue
}
if vol.Type == db.StoragePoolVolumeTypeNameVM {
volType = volTypeVM
} else if vol.Type == db.StoragePoolVolumeTypeNameCustom {
volType = volTypeCustom
} else {
continue
}
err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
return tx.UpdateStoragePoolVolume(ctx, vol.Project, vol.Name, volType, pool, vol.Description, config)
})
if err != nil {
return fmt.Errorf("Failed updating volume %q in project %q on pool %q: %w", vol.Name, vol.Project, poolIDNameMap[pool], err)
}
}
}
return nil
}
func patchStorageZfsUnsetInvalidBlockSettingsV2(_ string, d *Daemon) error {
s := d.State()
var pools []string
err := s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
var err error
pools, err = tx.GetStoragePoolNames(ctx)
return err
})
if err != nil {
if api.StatusErrorCheck(err, http.StatusNotFound) {
return nil
}
return fmt.Errorf("Failed getting storage pool names: %w", err)
}
volTypeCustom := db.StoragePoolVolumeTypeCustom
volTypeVM := db.StoragePoolVolumeTypeVM
poolIDNameMap := make(map[int64]string, 0)
poolVolumes := make(map[int64][]*db.StorageVolume, 0)
err = s.DB.Cluster.Transaction(s.ShutdownCtx, func(ctx context.Context, tx *db.ClusterTx) error {
for _, pool := range pools {
poolID, err := tx.GetStoragePoolID(ctx, pool)
if err != nil {
return fmt.Errorf("Failed getting storage pool ID of pool %q: %w", pool, err)
}
driverName, err := tx.GetStoragePoolDriver(ctx, poolID)
if err != nil {
return fmt.Errorf("Failed getting storage pool driver of pool %q: %w", pool, err)
}
if driverName != "zfs" {
continue
}
volumes, err := tx.GetStoragePoolVolumes(ctx, poolID, false, db.StorageVolumeFilter{Type: &volTypeCustom}, db.StorageVolumeFilter{Type: &volTypeVM})
if err != nil {
return fmt.Errorf("Failed getting custom storage volumes of pool %q: %w", pool, err)
}
if poolVolumes[poolID] == nil {
poolVolumes[poolID] = []*db.StorageVolume{}
}
poolIDNameMap[poolID] = pool
poolVolumes[poolID] = append(poolVolumes[poolID], volumes...)
}
return nil
})
if err != nil {
return err
}
var volType int
for pool, volumes := range poolVolumes {
for _, vol := range volumes {
if s.ServerName != "" && vol.Location != s.ServerName {
continue
}
config := vol.Config
if util.IsTrue(config["zfs.block_mode"]) && vol.Type == db.StoragePoolVolumeTypeNameCustom {
continue
}
update := false
for _, k := range []string{"block.filesystem", "block.mount_options"} {
_, found := config[k]
if found {
delete(config, k)
update = true
}
}
if !update {
continue
}
if vol.Type == db.StoragePoolVolumeTypeNameVM {
volType = volTypeVM
} else if vol.Type == db.StoragePoolVolumeTypeNameCustom {
volType = volTypeCustom
} else {
continue
}
err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
return tx.UpdateStoragePoolVolume(ctx, vol.Project, vol.Name, volType, pool, vol.Description, config)
})
if err != nil {
return fmt.Errorf("Failed updating volume %q in project %q on pool %q: %w", vol.Name, vol.Project, poolIDNameMap[pool], err)
}
}
}
return nil
}
func patchRuntimeDirectory(name string, d *Daemon) error {
s := d.State()
instances, err := instance.LoadNodeAll(s, instancetype.Any)
if err != nil {
return fmt.Errorf("Failed loading local instances: %w", err)
}
for _, inst := range instances {
if !util.PathExists(inst.LogPath()) {
continue
}
err = os.MkdirAll(inst.RunPath(), 0700)
if err != nil && !os.IsExist(err) {
return fmt.Errorf("Failed to create runtime directory for %q in project %q: %w", inst.Name(), inst.Project(), err)
}
files, err := os.ReadDir(inst.LogPath())
if err != nil {
return fmt.Errorf("Failed to list log files for %q in project %q: %w", inst.Name(), inst.Project(), err)
}
for _, fi := range files {
name := fi.Name()
if strings.HasSuffix(name, ".log") || strings.HasSuffix(name, ".log.old") || strings.HasSuffix(name, ".gz") {
continue
}
info, err := fi.Info()
if err != nil {
return fmt.Errorf("Failed getting file info on %q for instance %q in project %q: %w", name, inst.Name(), inst.Project(), err)
}
if info.Mode().IsRegular() {
_, err := subprocess.RunCommand("mv", filepath.Join(inst.LogPath(), name), inst.RunPath())
if err != nil {
return fmt.Errorf("Failed to relocate runtime file %q for instance %q in project %q: %w", name, inst.Name(), inst.Project(), err)
}
} else {
err := os.Symlink(filepath.Join(inst.LogPath(), name), filepath.Join(inst.RunPath(), name))
if err != nil {
return fmt.Errorf("Failed to symlink runtime file %q for instance %q in project %q: %w", name, inst.Name(), inst.Project(), err)
}
}
}
}
return nil
}
func patchLvmForceReuseKey(name string, d *Daemon) error {
revert := revert.New()
defer revert.Fail()
tx, err := d.db.Cluster.Begin()
if err != nil {
return fmt.Errorf("Failed to begin transaction: %w", err)
}
revert.Add(func() { _ = tx.Rollback() })
nodeIDs, err := query.SelectIntegers(context.TODO(), tx, "SELECT id FROM nodes")
if err != nil {
return fmt.Errorf("Failed to get IDs of current nodes: %w", err)
}
poolIDs, err := query.SelectIntegers(context.TODO(), tx, "SELECT id FROM storage_pools WHERE driver='lvm'")
if err != nil {
return fmt.Errorf("Failed to get IDs of current LVM pools: %w", err)
}
for _, poolID := range poolIDs {
config, err := query.SelectConfig(context.TODO(), tx, "storage_pools_config", "storage_pool_id=? AND node_id IS NULL", poolID)
if err != nil {
return fmt.Errorf("Failed to fetch of lvm pool config: %w", err)
}
value, ok := config["lvm.vg.force_reuse"]
if !ok {
continue
}
_, err = tx.Exec("DELETE FROM storage_pools_config WHERE key='lvm.vg.force_reuse' AND storage_pool_id=? AND node_id IS NULL", poolID)
if err != nil {
return fmt.Errorf("Failed to delete old config: %w", err)
}
for _, nodeID := range nodeIDs {
_, err := tx.Exec(`
INSERT INTO storage_pools_config(storage_pool_id, node_id, key, value)
VALUES(?, ?, 'lvm.vg.force_reuse', ?)
`, poolID, nodeID, value)
if err != nil {
return fmt.Errorf("Failed to create new config: %w", err)
}
}
}
err = tx.Commit()
if err != nil {
return fmt.Errorf("Failed to commit transaction: %w", err)
}
revert.Success()
return nil
}