Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Import LXD changes #243

Merged
merged 10 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 11 additions & 73 deletions cmd/incus/move.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,35 +170,24 @@ func (c *cmdMove) Run(cmd *cobra.Command, args []string) error {
}
}

// Support for server-side pool move.
if c.flagStorage != "" && sourceRemote == destRemote {
// Support for server-side move. Currently, such migration can only move an instance to different project
// or storage pool. If specific profile, device or config is provided, the instance should be copied (move using copy).
if sourceRemote == destRemote && c.flagStorage != "" || c.flagTargetProject != "" {
source, err := conf.GetInstanceServer(sourceRemote)
if err != nil {
return err
}

if source.HasExtension("instance_pool_move") {
if c.flagMode != moveDefaultMode {
return fmt.Errorf(i18n.G("The --mode flag can't be used with --storage"))
if source.HasExtension("instance_pool_move") && source.HasExtension("instance_project_move") {
if len(c.flagConfig) != 0 || len(c.flagDevice) != 0 || len(c.flagProfile) != 0 || c.flagNoProfiles {
return fmt.Errorf("The move command does not support flags --config, --device, --profile, and --no-profiles. Please use copy instead")
}

return moveInstancePool(conf, sourceResource, destResource, c.flagInstanceOnly, c.flagStorage, stateful)
}
}

// Support for server-side project move.
if c.flagTargetProject != "" && sourceRemote == destRemote {
source, err := conf.GetInstanceServer(sourceRemote)
if err != nil {
return err
}

if source.HasExtension("instance_project_move") {
if c.flagMode != moveDefaultMode {
return fmt.Errorf(i18n.G("The --mode flag can't be used with --target-project"))
return fmt.Errorf(i18n.G("The --mode flag can't be used with --storage or --target-project"))
}

return moveInstanceProject(conf, sourceResource, destResource, c.flagTargetProject, c.flagInstanceOnly, stateful)
return moveInstance(conf, sourceResource, destResource, c.flagStorage, c.flagTargetProject, c.flagInstanceOnly, stateful)
}
}

Expand Down Expand Up @@ -308,8 +297,8 @@ func moveClusterInstance(conf *config.Config, sourceResource string, destResourc
return nil
}

// Move an instance between pools using special POST /instances/<name> API.
func moveInstancePool(conf *config.Config, sourceResource string, destResource string, instanceOnly bool, storage string, stateful bool) error {
// Move an instance between pools and projects using special POST /instances/<name> API.
func moveInstance(conf *config.Config, sourceResource string, destResource string, storage string, targetProject string, instanceOnly bool, stateful bool) error {
// Parse the source.
sourceRemote, sourceName, err := conf.ParseRemote(sourceResource)
if err != nil {
Expand Down Expand Up @@ -342,60 +331,9 @@ func moveInstancePool(conf *config.Config, sourceResource string, destResource s
req := api.InstancePost{
Name: destName,
Migration: true,
Pool: storage,
InstanceOnly: instanceOnly,
Live: stateful,
}

op, err := source.MigrateInstance(sourceName, req)
if err != nil {
return fmt.Errorf(i18n.G("Migration API failure: %w"), err)
}

err = op.Wait()
if err != nil {
return fmt.Errorf(i18n.G("Migration operation failure: %w"), err)
}

return nil
}

// Move an instance between projects using special POST /instances/<name> API.
func moveInstanceProject(conf *config.Config, sourceResource string, destResource string, targetProject string, instanceOnly bool, stateful bool) error {
// Parse the source.
sourceRemote, sourceName, err := conf.ParseRemote(sourceResource)
if err != nil {
return err
}

// Parse the destination.
_, destName, err := conf.ParseRemote(destResource)
if err != nil {
return err
}

// Make sure we have an instance or snapshot name.
if sourceName == "" {
return fmt.Errorf(i18n.G("You must specify a source instance name"))
}

// The destination name is optional.
if destName == "" {
destName = sourceName
}

// Connect to the source host.
source, err := conf.GetInstanceServer(sourceRemote)
if err != nil {
return fmt.Errorf(i18n.G("Failed to connect to cluster member: %w"), err)
}

// Pass the new project to the migration API.
req := api.InstancePost{
Name: destName,
Migration: true,
Pool: storage,
Project: targetProject,
InstanceOnly: instanceOnly,
Live: stateful,
}

Expand Down
193 changes: 87 additions & 106 deletions cmd/incusd/instance_post.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -325,34 +326,19 @@ func instancePost(d *Daemon, r *http.Request) response.Response {
}

if req.Migration {
// Server-side pool migration.
if req.Pool != "" {
// Setup the instance move operation.
run := func(op *operations.Operation) error {
return instancePostPoolMigration(s, inst, req.Name, req.InstanceOnly, req.Pool, req.Live, req.AllowInconsistent, op)
}

resources := map[string][]api.URL{}
resources["instances"] = []api.URL{*api.NewURL().Path(version.APIVersion, "instances", name)}
op, err := operations.OperationCreate(s, projectName, operations.OperationClassTask, operationtype.InstanceMigrate, resources, nil, run, nil, nil, r)
if err != nil {
return response.InternalError(err)
}

return operations.OperationResponse(op)
}

// Server-side project migration.
if req.Project != "" {
// Check if user has access to target project
err := s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectProject(req.Project), auth.EntitlementCanCreateInstances)
if err != nil {
return response.SmartError(err)
// Server-side instance migration.
if req.Pool != "" || req.Project != "" {
// Check if user has access to target project.
if req.Project != "" {
err := s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectProject(req.Project), auth.EntitlementCanCreateInstances)
if err != nil {
return response.SmartError(err)
}
}

// Setup the instance move operation.
run := func(op *operations.Operation) error {
return instancePostProjectMigration(s, inst, req.Name, req.Project, req.InstanceOnly, req.Live, req.AllowInconsistent, op)
return instancePostMigration(s, inst, req.Name, req.Pool, req.Project, req.InstanceOnly, req.Live, req.AllowInconsistent, op)
}

resources := map[string][]api.URL{}
Expand Down Expand Up @@ -462,12 +448,16 @@ func instancePost(d *Daemon, r *http.Request) response.Response {
return operations.OperationResponse(op)
}

// Move an instance to another pool.
func instancePostPoolMigration(s *state.State, inst instance.Instance, newName string, instanceOnly bool, newPool string, stateful bool, allowInconsistent bool, op *operations.Operation) error {
// Move an instance.
func instancePostMigration(s *state.State, inst instance.Instance, newName string, newPool string, newProject string, instanceOnly bool, stateful bool, allowInconsistent bool, op *operations.Operation) error {
if inst.IsSnapshot() {
return fmt.Errorf("Instance snapshots cannot be moved between pools")
}

if newProject == "" {
newProject = inst.Project().Name
}

statefulStart := false
if inst.IsRunning() {
if stateful {
Expand All @@ -487,106 +477,78 @@ func instancePostPoolMigration(s *state.State, inst instance.Instance, newName s
localConfig[k] = v
}

// Load source root disk from expanded devices (in case instance doesn't have its own root disk).
rootDevKey, rootDev, err := internalInstance.GetRootDiskDevice(inst.ExpandedDevices().CloneNative())
if err != nil {
localDevices := inst.LocalDevices().Clone()

// Check if root disk device is present in the instance config. If instance config has not
// root disk device configured, check if any of the profiles that will be applied in the
// target project contain a root disk device. Lastly, set current root disk device in the
// instance's config.
rootDevKey, rootDev, err := internalInstance.GetRootDiskDevice(inst.LocalDevices().CloneNative())
if err != nil && !errors.Is(err, internalInstance.ErrNoRootDisk) {
return err
}
} else if errors.Is(err, internalInstance.ErrNoRootDisk) {
instProfiles := make([]string, 0, len(inst.Profiles()))
for _, p := range inst.Profiles() {
instProfiles = append(instProfiles, p.Name)
}

// Copy device config from instance, and update target instance root disk device with the new pool name.
localDevices := inst.LocalDevices().Clone()
rootDev["pool"] = newPool
localDevices[rootDevKey] = rootDev
// Find profiles that will be applied in target project and check if any of them contains
// root disk device.
err := s.DB.Cluster.Transaction(s.ShutdownCtx, func(ctx context.Context, tx *db.ClusterTx) error {
profiles, err := dbCluster.GetProfilesIfEnabled(ctx, tx.Tx(), newProject, instProfiles)
if err != nil {
return err
}

// Specify the target instance config with the new name and modified root disk config.
args := db.InstanceArgs{
Name: newName,
BaseImage: localConfig["volatile.base_image"],
Config: localConfig,
Devices: localDevices,
Project: inst.Project().Name,
Type: inst.Type(),
Architecture: inst.Architecture(),
Description: inst.Description(),
Ephemeral: inst.IsEphemeral(),
Profiles: inst.Profiles(),
Stateful: inst.IsStateful(),
}
for _, p := range profiles {
// Get disk devices of the matching profile.
devDiskType := dbCluster.TypeDisk
devDiskfilter := dbCluster.DeviceFilter{
Type: &devDiskType,
}

// If we are moving the instance to a new pool but keeping the same instance name, then we need to create
// the copy of the instance on the new pool with a temporary name that is different from the source to
// avoid conflicts. Then after the source instance has been deleted we will rename the new instance back
// to the original name.
if newName == inst.Name() {
args.Name, err = instance.MoveTemporaryName(inst)
if err != nil {
return err
}
}
disks, err := dbCluster.GetProfileDevices(ctx, tx.Tx(), p.ID, devDiskfilter)
if err != nil {
return err
}

// Copy instance to new target instance.
targetInst, err := instanceCreateAsCopy(s, instanceCreateAsCopyOpts{
sourceInstance: inst,
targetInstance: args,
instanceOnly: instanceOnly,
applyTemplateTrigger: false, // Don't apply templates when moving.
allowInconsistent: allowInconsistent,
}, op)
if err != nil {
return err
}
devices := make(map[string]map[string]string)
for _, d := range disks {
devices[d.Name] = d.Config
}

// Delete original instance.
err = inst.Delete(true)
if err != nil {
return err
}
rootDevKey, rootDev, err = internalInstance.GetRootDiskDevice(devices)
if err != nil {
continue
}

// Rename copy from temporary name to original name if needed.
if newName == inst.Name() {
err = targetInst.Rename(newName, false) // Don't apply templates when moving.
if err != nil {
return err
}
}
break
}

if statefulStart {
err = targetInst.Start(true)
return nil
})
if err != nil {
return err
}
}

return nil
}

// Move an instance to another project.
func instancePostProjectMigration(s *state.State, inst instance.Instance, newName string, newProject string, instanceOnly bool, stateful bool, allowInconsistent bool, op *operations.Operation) error {
localConfig := inst.LocalConfig()

statefulStart := false
if inst.IsRunning() {
if stateful {
statefulStart = true
err := inst.Stop(true)
// If root disk device was not found in target project profiles, apply current root disk device
// to the instance's config.
if rootDev == nil {
rootDevKey, rootDev, err = internalInstance.GetRootDiskDevice(inst.ExpandedDevices().CloneNative())
if err != nil {
return err
}
} else {
return api.StatusErrorf(http.StatusBadRequest, "Instance must be stopped to move between projects statelessly")

localDevices[rootDevKey] = rootDev
}
}

// Load source root disk from expanded devices (in case instance doesn't have its own root disk).
rootDevKey, rootDev, err := internalInstance.GetRootDiskDevice(inst.ExpandedDevices().CloneNative())
if err != nil {
return err
// Set specific storage pool for the instance, if provided.
if newPool != "" {
rootDev["pool"] = newPool
localDevices[rootDevKey] = rootDev
}

// Copy device config from instance
localDevices := inst.LocalDevices().Clone()
localDevices[rootDevKey] = rootDev

// Specify the target instance config with the new name.
args := db.InstanceArgs{
Name: newName,
Expand All @@ -602,6 +564,17 @@ func instancePostProjectMigration(s *state.State, inst instance.Instance, newNam
Stateful: inst.IsStateful(),
}

// If we are moving the instance to a new pool but keeping the same instance name, then we need to create
// the copy of the instance on the new pool with a temporary name that is different from the source to
// avoid conflicts. Then after the source instance has been deleted we will rename the new instance back
// to the original name.
if newName == inst.Name() && newProject == inst.Project().Name {
args.Name, err = instance.MoveTemporaryName(inst)
if err != nil {
return err
}
}

// Copy instance to new target instance.
targetInst, err := instanceCreateAsCopy(s, instanceCreateAsCopyOpts{
sourceInstance: inst,
Expand All @@ -620,6 +593,14 @@ func instancePostProjectMigration(s *state.State, inst instance.Instance, newNam
return err
}

// Rename copy from temporary name to original name if needed.
if newName == inst.Name() && newProject == inst.Project().Name {
err = targetInst.Rename(newName, false) // Don't apply templates when moving.
if err != nil {
return err
}
}

if statefulStart {
err = targetInst.Start(true)
if err != nil {
Expand Down
Loading
Loading