package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"slices"
internalInstance "github.com/lxc/incus/v6/internal/instance"
internalRecover "github.com/lxc/incus/v6/internal/recover"
"github.com/lxc/incus/v6/internal/revert"
"github.com/lxc/incus/v6/internal/server/auth"
"github.com/lxc/incus/v6/internal/server/backup"
backupConfig "github.com/lxc/incus/v6/internal/server/backup/config"
"github.com/lxc/incus/v6/internal/server/db"
dbCluster "github.com/lxc/incus/v6/internal/server/db/cluster"
deviceConfig "github.com/lxc/incus/v6/internal/server/device/config"
"github.com/lxc/incus/v6/internal/server/instance"
"github.com/lxc/incus/v6/internal/server/instance/instancetype"
"github.com/lxc/incus/v6/internal/server/project"
"github.com/lxc/incus/v6/internal/server/response"
"github.com/lxc/incus/v6/internal/server/state"
storagePools "github.com/lxc/incus/v6/internal/server/storage"
storageDrivers "github.com/lxc/incus/v6/internal/server/storage/drivers"
"github.com/lxc/incus/v6/shared/api"
"github.com/lxc/incus/v6/shared/logger"
"github.com/lxc/incus/v6/shared/osarch"
)
var internalRecoverValidateCmd = APIEndpoint{
Path: "recover/validate",
Post: APIEndpointAction{Handler: internalRecoverValidate, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)},
}
var internalRecoverImportCmd = APIEndpoint{
Path: "recover/import",
Post: APIEndpointAction{Handler: internalRecoverImport, AccessHandler: allowPermission(auth.ObjectTypeServer, auth.EntitlementCanEdit)},
}
func init() {
apiInternal = append(apiInternal, internalRecoverValidateCmd, internalRecoverImportCmd)
}
func internalRecoverScan(ctx context.Context, s *state.State, userPools []api.StoragePoolsPost, validateOnly bool) response.Response {
var err error
var projects map[string]*api.Project
var projectProfiles map[string][]*api.Profile
var projectNetworks map[string]map[int64]api.Network
err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {
ps, err := dbCluster.GetProjects(ctx, tx.Tx())
if err != nil {
return err
}
projects = make(map[string]*api.Project, len(ps))
for i := range ps {
project, err := ps[i].ToAPI(ctx, tx.Tx())
if err != nil {
return err
}
projects[ps[i].Name] = project
}
profiles, err := dbCluster.GetProfiles(ctx, tx.Tx())
if err != nil {
return err
}
profileDevices, err := dbCluster.GetDevices(ctx, tx.Tx(), "profile")
if err != nil {
return err
}
projectProfiles = make(map[string][]*api.Profile)
for _, profile := range profiles {
if projectProfiles[profile.Project] == nil {
projectProfiles[profile.Project] = []*api.Profile{}
}
apiProfile, err := profile.ToAPI(ctx, tx.Tx(), profileDevices)
if err != nil {
return err
}
projectProfiles[profile.Project] = append(projectProfiles[profile.Project], apiProfile)
}
projectNetworks, err = tx.GetCreatedNetworks(ctx)
if err != nil {
return err
}
return nil
})
if err != nil {
return response.SmartError(fmt.Errorf("Failed getting validate dependency check info: %w", err))
}
res := internalRecover.ValidateResult{}
revert := revert.New()
defer revert.Fail()
addDependencyError := func(err error) {
errStr := err.Error()
if !slices.Contains(res.DependencyErrors, errStr) {
res.DependencyErrors = append(res.DependencyErrors, errStr)
}
}
poolsProjectVols := make(map[string]map[string][]*backupConfig.Config)
pools := make(map[string]storagePools.Pool)
for _, p := range userPools {
pool, err := storagePools.LoadByName(s, p.Name)
if err != nil {
if response.IsNotFoundError(err) {
if s.ServerClustered {
return response.BadRequest(fmt.Errorf("Storage pool recovery not supported when clustered"))
}
poolInfo := api.StoragePool{
Name: p.Name,
Driver: p.Driver,
StoragePoolPut: p.StoragePoolPut,
Status: api.StoragePoolStatusCreated,
}
pool, err = storagePools.NewTemporary(s, &poolInfo)
if err != nil {
return response.SmartError(fmt.Errorf("Failed to initialize unknown pool %q: %w", p.Name, err))
}
err := pool.Driver().FillConfig()
if err != nil {
return response.SmartError(fmt.Errorf("Failed to evaluate the default configuration values for unknown pool %q: %w", p.Name, err))
}
err = pool.Driver().Validate(poolInfo.Config)
if err != nil {
return response.SmartError(fmt.Errorf("Failed config validation for unknown pool %q: %w", p.Name, err))
}
} else {
return response.SmartError(fmt.Errorf("Failed loading existing pool %q: %w", p.Name, err))
}
}
pools[p.Name] = pool
ourMount, err := pool.Mount()
if err != nil {
return response.SmartError(fmt.Errorf("Failed mounting pool %q: %w", pool.Name(), err))
}
if ourMount {
defer func() {
cleanupPool := pools[pool.Name()]
if cleanupPool != nil && cleanupPool.ID() == storagePools.PoolIDTemporary {
_, _ = cleanupPool.Unmount()
}
}()
revert.Add(func() {
cleanupPool := pools[pool.Name()]
_, _ = cleanupPool.Unmount()
})
}
poolProjectVols, err := pool.ListUnknownVolumes(nil)
if err != nil {
if errors.Is(err, storageDrivers.ErrNotSupported) {
continue
}
return response.SmartError(fmt.Errorf("Failed checking volumes on pool %q: %w", pool.Name(), err))
}
poolsProjectVols[p.Name] = poolProjectVols
for projectName, poolVols := range poolProjectVols {
projectInfo := projects[projectName]
var profileProjectname string
var networkProjectName string
if projectInfo != nil {
profileProjectname = project.ProfileProjectFromRecord(projectInfo)
networkProjectName = project.NetworkProjectFromRecord(projectInfo)
} else {
addDependencyError(fmt.Errorf("Project %q", projectName))
continue
}
for _, poolVol := range poolVols {
if poolVol.Container == nil {
continue
}
for _, poolInstProfileName := range poolVol.Container.Profiles {
foundProfile := false
for _, profile := range projectProfiles[profileProjectname] {
if profile.Name == poolInstProfileName {
foundProfile = true
}
}
if !foundProfile {
addDependencyError(fmt.Errorf("Profile %q in project %q", poolInstProfileName, projectName))
}
}
for _, devConfig := range poolVol.Container.ExpandedDevices {
if devConfig["type"] != "nic" {
continue
}
if devConfig["network"] == "" {
continue
}
foundNetwork := false
for _, n := range projectNetworks[networkProjectName] {
if n.Name == devConfig["network"] {
foundNetwork = true
break
}
}
if !foundNetwork {
addDependencyError(fmt.Errorf("Network %q in project %q", devConfig["network"], projectName))
}
}
}
}
}
if validateOnly || len(res.DependencyErrors) > 0 {
for poolName, poolProjectVols := range poolsProjectVols {
for projectName, poolVols := range poolProjectVols {
for _, poolVol := range poolVols {
var displayType, displayName string
var displaySnapshotCount int
if poolVol.Container != nil {
displayType = poolVol.Container.Type
displayName = poolVol.Container.Name
displaySnapshotCount = len(poolVol.Snapshots)
} else if poolVol.Bucket != nil {
displayType = "bucket"
displayName = poolVol.Bucket.Name
displaySnapshotCount = 0
} else {
displayType = "volume"
displayName = poolVol.Volume.Name
displaySnapshotCount = len(poolVol.VolumeSnapshots)
}
res.UnknownVolumes = append(res.UnknownVolumes, internalRecover.ValidateVolume{
Pool: poolName,
Project: projectName,
Type: displayType,
Name: displayName,
SnapshotCount: displaySnapshotCount,
})
}
}
}
return response.SyncResponse(true, &res)
}
for _, pool := range pools {
if pool.ID() == storagePools.PoolIDTemporary {
var instPoolVol *backupConfig.Config
var poolID int64
var poolVols []*backupConfig.Config
for _, value := range poolsProjectVols[pool.Name()] {
poolVols = append(poolVols, value...)
}
for _, poolVol := range poolVols {
if poolVol.Pool != nil && poolVol.Pool.Config != nil {
instPoolVol = poolVol
break
}
}
if instPoolVol != nil {
logger.Info("Creating storage pool DB record from instance config", logger.Ctx{"name": instPoolVol.Pool.Name, "description": instPoolVol.Pool.Description, "driver": instPoolVol.Pool.Driver, "config": instPoolVol.Pool.Config})
poolID, err = dbStoragePoolCreateAndUpdateCache(ctx, s, instPoolVol.Pool.Name, instPoolVol.Pool.Description, instPoolVol.Pool.Driver, instPoolVol.Pool.Config)
if err != nil {
return response.SmartError(fmt.Errorf("Failed creating storage pool %q database entry: %w", pool.Name(), err))
}
} else {
poolDriverName := pool.Driver().Info().Name
poolDriverConfig := pool.Driver().Config()
logger.Info("Creating storage pool DB record from user config", logger.Ctx{"name": pool.Name(), "driver": poolDriverName, "config": poolDriverConfig})
poolID, err = dbStoragePoolCreateAndUpdateCache(ctx, s, pool.Name(), "", poolDriverName, poolDriverConfig)
if err != nil {
return response.SmartError(fmt.Errorf("Failed creating storage pool %q database entry: %w", pool.Name(), err))
}
}
revert.Add(func() {
_ = dbStoragePoolDeleteAndUpdateCache(context.Background(), s, pool.Name())
})
err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {
return tx.StoragePoolNodeCreated(poolID)
})
if err != nil {
return response.SmartError(fmt.Errorf("Failed marking storage pool %q local status as created: %w", pool.Name(), err))
}
logger.Debug("Marked storage pool local status as created", logger.Ctx{"pool": pool.Name()})
newPool, err := storagePools.LoadByName(s, pool.Name())
if err != nil {
return response.SmartError(fmt.Errorf("Failed loading created storage pool %q: %w", pool.Name(), err))
}
pools[pool.Name()] = newPool
}
}
for _, pool := range pools {
for projectName, poolVols := range poolsProjectVols[pool.Name()] {
projectInfo := projects[projectName]
if projectInfo == nil {
return response.SmartError(fmt.Errorf("Project %q not found", projectName))
}
customStorageProjectName := project.StorageVolumeProjectFromRecord(projectInfo, db.StoragePoolVolumeTypeCustom)
for _, poolVol := range poolVols {
if poolVol.Container != nil || poolVol.Bucket != nil {
continue
} else if poolVol.Container == nil && poolVol.Volume == nil {
return response.SmartError(fmt.Errorf("Volume is neither instance nor custom volume"))
}
cleanup, err := pool.ImportCustomVolume(customStorageProjectName, poolVol, nil)
if err != nil {
return response.SmartError(fmt.Errorf("Failed importing custom volume %q in project %q: %w", poolVol.Volume.Name, projectName, err))
}
revert.Add(cleanup)
}
for _, poolVol := range poolVols {
if poolVol.Bucket == nil {
continue
}
cleanup, err := pool.ImportBucket(projectName, poolVol, nil)
if err != nil {
return response.SmartError(fmt.Errorf("Failed importing bucket %q in project %q: %w", poolVol.Bucket.Name, projectName, err))
}
revert.Add(cleanup)
}
}
}
for _, pool := range pools {
for projectName, poolVols := range poolsProjectVols[pool.Name()] {
projectInfo := projects[projectName]
if projectInfo == nil {
return response.SmartError(fmt.Errorf("Project %q not found", projectName))
}
profileProjectName := project.ProfileProjectFromRecord(projectInfo)
for _, poolVol := range poolVols {
if poolVol.Container == nil && (poolVol.Volume != nil || poolVol.Bucket != nil) {
continue
}
profiles := make([]api.Profile, 0, len(poolVol.Container.Profiles))
for _, profileName := range poolVol.Container.Profiles {
for i := range projectProfiles[profileProjectName] {
if projectProfiles[profileProjectName][i].Name == profileName {
profiles = append(profiles, *projectProfiles[profileProjectName][i])
}
}
}
inst, cleanup, err := internalRecoverImportInstance(s, pool, projectName, poolVol, profiles)
if err != nil {
return response.SmartError(fmt.Errorf("Failed creating instance %q record in project %q: %w", poolVol.Container.Name, projectName, err))
}
revert.Add(cleanup)
for _, poolInstSnap := range poolVol.Snapshots {
profiles := make([]api.Profile, 0, len(poolInstSnap.Profiles))
for _, profileName := range poolInstSnap.Profiles {
for i := range projectProfiles[profileProjectName] {
if projectProfiles[profileProjectName][i].Name == profileName {
profiles = append(profiles, *projectProfiles[profileProjectName][i])
}
}
}
cleanup, err := internalRecoverImportInstanceSnapshot(s, pool, projectName, poolVol, poolInstSnap, profiles)
if err != nil {
return response.SmartError(fmt.Errorf("Failed creating instance %q snapshot %q record in project %q: %w", poolVol.Container.Name, poolInstSnap.Name, projectName, err))
}
revert.Add(cleanup)
}
cleanup, err = pool.ImportInstance(inst, poolVol, nil)
if err != nil {
return response.SmartError(fmt.Errorf("Failed importing instance %q in project %q: %w", poolVol.Container.Name, projectName, err))
}
revert.Add(cleanup)
_, rootConfig, err := internalInstance.GetRootDiskDevice(inst.ExpandedDevices().CloneNative())
if err == nil {
err = pool.SetInstanceQuota(inst, rootConfig["size"], rootConfig["size.state"], nil)
if err != nil {
return response.SmartError(fmt.Errorf("Failed reinitializing root disk quota %q for instance %q in project %q: %w", rootConfig["size"], poolVol.Container.Name, projectName, err))
}
}
}
}
}
revert.Success()
return response.EmptySyncResponse
}
func internalRecoverImportInstance(s *state.State, pool storagePools.Pool, projectName string, poolVol *backupConfig.Config, profiles []api.Profile) (instance.Instance, revert.Hook, error) {
if poolVol.Container == nil {
return nil, nil, fmt.Errorf("Pool volume is not an instance volume")
}
if poolVol.Container.Devices == nil {
poolVol.Container.Devices = make(map[string]map[string]string, 0)
}
if poolVol.Container.ExpandedDevices == nil {
poolVol.Container.ExpandedDevices = make(map[string]map[string]string, 0)
}
internalImportRootDevicePopulate(pool.Name(), poolVol.Container.Devices, poolVol.Container.ExpandedDevices, profiles)
dbInst, err := backup.ConfigToInstanceDBArgs(s, poolVol, projectName, true)
if err != nil {
return nil, nil, err
}
if dbInst.Type < 0 {
return nil, nil, fmt.Errorf("Invalid instance type")
}
inst, instOp, cleanup, err := instance.CreateInternal(s, *dbInst, nil, false, true)
if err != nil {
return nil, nil, fmt.Errorf("Failed creating instance record: %w", err)
}
defer instOp.Done(err)
return inst, cleanup, err
}
func internalRecoverImportInstanceSnapshot(s *state.State, pool storagePools.Pool, projectName string, poolVol *backupConfig.Config, snap *api.InstanceSnapshot, profiles []api.Profile) (revert.Hook, error) {
if poolVol.Container == nil || snap == nil {
return nil, fmt.Errorf("Pool volume is not an instance volume")
}
if snap.Devices == nil {
snap.Devices = make(map[string]map[string]string, 0)
}
if snap.ExpandedDevices == nil {
snap.ExpandedDevices = make(map[string]map[string]string, 0)
}
internalImportRootDevicePopulate(pool.Name(), snap.Devices, snap.ExpandedDevices, profiles)
arch, err := osarch.ArchitectureId(snap.Architecture)
if err != nil {
return nil, err
}
instanceType, err := instancetype.New(poolVol.Container.Type)
if err != nil {
return nil, err
}
_, snapInstOp, cleanup, err := instance.CreateInternal(s, db.InstanceArgs{
Project: projectName,
Architecture: arch,
BaseImage: snap.Config["volatile.base_image"],
Config: snap.Config,
CreationDate: snap.CreatedAt,
Type: instanceType,
Snapshot: true,
Devices: deviceConfig.NewDevices(snap.Devices),
Ephemeral: snap.Ephemeral,
LastUsedDate: snap.LastUsedAt,
Name: poolVol.Container.Name + internalInstance.SnapshotDelimiter + snap.Name,
Profiles: profiles,
Stateful: snap.Stateful,
}, nil, false, true)
if err != nil {
return nil, fmt.Errorf("Failed creating instance snapshot record %q: %w", snap.Name, err)
}
defer snapInstOp.Done(err)
return cleanup, err
}
func internalRecoverValidate(d *Daemon, r *http.Request) response.Response {
req := &internalRecover.ValidatePost{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
return response.BadRequest(err)
}
return internalRecoverScan(r.Context(), d.State(), req.Pools, true)
}
func internalRecoverImport(d *Daemon, r *http.Request) response.Response {
req := &internalRecover.ImportPost{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
return response.BadRequest(err)
}
return internalRecoverScan(r.Context(), d.State(), req.Pools, false)
}