1
0
mirror of https://github.com/lxc/incus.git synced 2026-02-05 09:46:19 +01:00
Files
incus/cmd/incus-migrate/migrate_instance.go

573 lines
13 KiB
Go
Raw Normal View History

package main
import (
"bufio"
"context"
"errors"
"fmt"
"os"
"os/exec"
"slices"
"strings"
"gopkg.in/yaml.v2"
incus "github.com/lxc/incus/v6/client"
"github.com/lxc/incus/v6/shared/api"
"github.com/lxc/incus/v6/shared/ask"
cli "github.com/lxc/incus/v6/shared/cmd"
"github.com/lxc/incus/v6/shared/osarch"
"github.com/lxc/incus/v6/shared/revert"
"github.com/lxc/incus/v6/shared/units"
"github.com/lxc/incus/v6/shared/util"
)
// InstanceMigration handles the migration logic for an instance.
type InstanceMigration struct {
*Migration
flagRsyncArgs string
instanceArgs api.InstancesPost
volumes []*VolumeMigration
}
// NewInstanceMigration returns a new InstanceMigration.
func NewInstanceMigration(ctx context.Context, server incus.InstanceServer, asker ask.Asker, flafRsyncArgs string, migraionType MigrationType) Migrator {
return &InstanceMigration{
Migration: &Migration{
asker: asker,
ctx: ctx,
server: server,
migrationType: migraionType,
},
flagRsyncArgs: flafRsyncArgs,
}
}
// gatherInfo collects information from the user about the instance to be created.
func (m *InstanceMigration) gatherInfo() error {
var err error
// Quick checks.
if m.migrationType == MigrationTypeContainer {
if os.Geteuid() != 0 {
return errors.New("This tool must be run as root for container migrations")
}
_, err := exec.LookPath("rsync")
if err != nil {
return errors.New("Unable to find required command \"rsync\"")
}
}
m.instanceArgs = api.InstancesPost{
Source: api.InstanceSource{
Type: "migration",
Mode: "push",
},
}
m.instanceArgs.Config = map[string]string{}
m.instanceArgs.Devices = map[string]map[string]string{}
if m.migrationType == MigrationTypeVM {
m.instanceArgs.Type = api.InstanceTypeVM
} else {
m.instanceArgs.Type = api.InstanceTypeContainer
}
// Project
err = m.askProject("Project to create the instance in [default=default]: ")
if err != nil {
return err
}
if m.project != "" {
m.server = m.server.UseProject(m.project)
}
// Target
err = m.askTarget()
if err != nil {
return err
}
m.server = m.server.UseTarget(m.target)
// Instance name
instanceNames, err := m.server.GetInstanceNames(api.InstanceTypeAny)
if err != nil {
return err
}
for {
instanceName, err := m.asker.AskString("Name of the new instance: ", "", nil)
if err != nil {
return err
}
if slices.Contains(instanceNames, instanceName) {
fmt.Printf("Instance %q already exists\n", instanceName)
continue
}
m.instanceArgs.Name = instanceName
break
}
var question string
// Provide source path
if m.migrationType == MigrationTypeVM || m.migrationType == MigrationTypeVolumeBlock {
question = "Please provide the path or URL to a disk, partition, or qcow2/raw/vmdk image file: "
} else {
question = "Please provide the path to a root filesystem: "
}
// Provide source path
err = m.askSourcePath(question)
if err != nil {
return err
}
err = m.setSourceFormat()
if err != nil {
return err
}
err = m.askUEFISupport()
if err != nil {
return err
}
var mounts []string
// Additional mounts for containers
if m.instanceArgs.Type == api.InstanceTypeContainer {
addMounts, err := m.asker.AskBool("Do you want to add additional filesystem mounts? [default=no]: ", "no")
if err != nil {
return err
}
if addMounts {
for {
path, err := m.asker.AskString("Please provide a path the filesystem mount path [empty value to continue]: ", "", func(s string) error {
if s != "" {
if util.PathExists(s) {
return nil
}
return errors.New("Path does not exist")
}
return nil
})
if err != nil {
return err
}
if path == "" {
break
}
mounts = append(mounts, path)
}
m.mounts = append(m.mounts, mounts...)
}
}
return nil
}
// migrate performs the instance migration.
func (m *InstanceMigration) migrate() error {
if m.migrationType != MigrationTypeVM && m.migrationType != MigrationTypeContainer {
return errors.New("Wrong migration type for migrate")
}
// Prioritize migrating all additional disks before the main instance.
for _, vol := range m.volumes {
err := vol.migrate()
if err != nil {
return err
}
}
return m.runMigration(func(path string) error {
// System architecture
architectureName, err := osarch.ArchitectureGetLocal()
if err != nil {
return err
}
m.instanceArgs.Architecture = architectureName
reverter := revert.New()
defer reverter.Fail()
// Create the instance
op, err := m.server.CreateInstance(m.instanceArgs)
if err != nil {
return err
}
reverter.Add(func() {
_, _ = m.server.DeleteInstance(m.instanceArgs.Name)
})
progress := cli.ProgressRenderer{Format: "Transferring instance: %s"}
_, err = op.AddHandler(progress.UpdateOp)
if err != nil {
progress.Done("")
return err
}
err = transferRootfs(m.ctx, op, path, m.flagRsyncArgs, m.migrationType)
if err != nil {
return err
}
progress.Done(fmt.Sprintf("Instance %s successfully created", m.instanceArgs.Name))
reverter.Success()
return nil
})
}
// renderObject renders the state of the instance.
func (m *InstanceMigration) renderObject() error {
for {
fmt.Println("\nInstance to be created:")
scanner := bufio.NewScanner(strings.NewReader(m.render()))
for scanner.Scan() {
fmt.Printf(" %s\n", scanner.Text())
}
fmt.Print(`
Additional overrides can be applied at this stage:
1) Begin the migration with the above configuration
2) Override profile list
3) Set additional configuration options
4) Change instance storage pool or volume size
5) Change instance network
6) Add additional disk
7) Change additional disk storage pool
`)
choice, err := m.asker.AskInt("Please pick one of the options above [default=1]: ", 1, 6, "1", nil)
if err != nil {
return err
}
switch choice {
case 1:
return nil
case 2:
err = m.askProfiles()
case 3:
err = m.askConfig()
case 4:
err = m.askStorage()
case 5:
err = m.askNetwork()
case 6:
err = m.askDisk()
case 7:
err = m.askDiskStorage()
}
if err != nil {
fmt.Println(err)
}
}
}
func (m *InstanceMigration) render() string {
data := struct {
Name string `yaml:"Name"`
Project string `yaml:"Project"`
Type api.InstanceType `yaml:"Type"`
Source string `yaml:"Source"`
SourceFormat string `yaml:"Source format,omitempty"`
Mounts []string `yaml:"Mounts,omitempty"`
Profiles []string `yaml:"Profiles,omitempty"`
StoragePool string `yaml:"Storage pool,omitempty"`
StorageSize string `yaml:"Storage pool size,omitempty"`
Network string `yaml:"Network name,omitempty"`
Config map[string]string `yaml:"Config,omitempty"`
Disks map[string]map[string]string `yaml:"Disks,omitempty"`
}{
m.instanceArgs.Name,
m.project,
m.instanceArgs.Type,
m.sourcePath,
m.sourceFormat,
m.mounts,
m.instanceArgs.Profiles,
"",
"",
"",
m.instanceArgs.Config,
make(map[string]map[string]string),
}
disk, ok := m.instanceArgs.Devices["root"]
if ok {
data.StoragePool = disk["pool"]
size, ok := disk["size"]
if ok {
data.StorageSize = size
}
}
network, ok := m.instanceArgs.Devices["eth0"]
if ok {
data.Network = network["parent"]
}
for k, v := range m.instanceArgs.Devices {
if v["type"] != "disk" || v["path"] == "/" {
continue
}
data.Disks[k] = v
}
out, err := yaml.Marshal(&data)
if err != nil {
return ""
}
return string(out)
}
func (m *InstanceMigration) askProfiles() error {
profileNames, err := m.server.GetProfileNames()
if err != nil {
return err
}
profiles, err := m.asker.AskString("Which profiles do you want to apply to the instance? (space separated) [default=default, \"-\" for none]: ", "default", func(s string) error {
// This indicates that no profiles should be applied.
if s == "-" {
return nil
}
profiles := strings.Split(s, " ")
for _, profile := range profiles {
if !slices.Contains(profileNames, profile) {
return fmt.Errorf("Unknown profile %q", profile)
}
}
return nil
})
if err != nil {
return err
}
if profiles != "-" {
m.instanceArgs.Profiles = strings.Split(profiles, " ")
}
return nil
}
func (m *InstanceMigration) askConfig() error {
configs, err := m.asker.AskString("Please specify config keys and values (key=value ...): ", "", func(s string) error {
if s == "" {
return nil
}
for _, entry := range strings.Split(s, " ") {
if !strings.Contains(entry, "=") {
return fmt.Errorf("Bad key=value configuration: %v", entry)
}
}
return nil
})
if err != nil {
return err
}
for _, entry := range strings.Split(configs, " ") {
key, value, _ := strings.Cut(entry, "=")
m.instanceArgs.Config[key] = value
}
return nil
}
func (m *InstanceMigration) askStorage() error {
storagePools, err := m.server.GetStoragePoolNames()
if err != nil {
return err
}
if len(storagePools) == 0 {
return errors.New("No storage pools available")
}
storagePool, err := m.asker.AskChoice("Please provide the storage pool to use: ", storagePools, "")
if err != nil {
return err
}
m.instanceArgs.Devices["root"] = map[string]string{
"type": "disk",
"pool": storagePool,
"path": "/",
}
changeStorageSize, err := m.asker.AskBool("Do you want to change the storage size? [default=no]: ", "no")
if err != nil {
return err
}
if changeStorageSize {
size, err := m.asker.AskString("Please specify the storage size: ", "", func(s string) error {
_, err := units.ParseByteSizeString(s)
return err
})
if err != nil {
return err
}
m.instanceArgs.Devices["root"]["size"] = size
}
return nil
}
func (m *InstanceMigration) askDiskStorage() error {
diskNames := []string{}
for _, vol := range m.volumes {
diskNames = append(diskNames, vol.customVolumeArgs.Name)
}
if len(diskNames) == 0 {
return errors.New("No additional disks available")
}
diskName, err := m.asker.AskChoice("Please provide the disk name: ", diskNames, "")
if err != nil {
return err
}
storagePools, err := m.server.GetStoragePoolNames()
if err != nil {
return err
}
if len(storagePools) == 0 {
return errors.New("No storage pools available")
}
storagePool, err := m.asker.AskChoice("Please provide the storage pool to use: ", storagePools, "")
if err != nil {
return err
}
m.instanceArgs.Devices[diskName]["pool"] = storagePool
for _, vol := range m.volumes {
if vol.customVolumeArgs.Name == diskName {
vol.pool = storagePool
break
}
}
return nil
}
func (m *InstanceMigration) askNetwork() error {
networks, err := m.server.GetNetworkNames()
if err != nil {
return err
}
network, err := m.asker.AskChoice("Please specify the network to use for the instance: ", networks, "")
if err != nil {
return err
}
m.instanceArgs.Devices["eth0"] = map[string]string{
"type": "nic",
"nictype": "bridged",
"parent": network,
"name": "eth0",
}
return nil
}
func (m *InstanceMigration) askDisk() error {
volMigrator, ok := NewVolumeMigration(m.ctx, m.server, m.asker, m.flagRsyncArgs).(*VolumeMigration)
if !ok {
return errors.New("Migrator should be of type VolumeMigration")
}
volMigrator.project = m.project
err := volMigrator.gatherInfo()
if err != nil {
return err
}
if m.migrationType == MigrationTypeContainer && volMigrator.migrationType == MigrationTypeVolumeBlock {
return errors.New("Block disk is not supported by the container")
}
m.instanceArgs.Devices[volMigrator.customVolumeArgs.Name] = map[string]string{
"type": "disk",
"pool": volMigrator.pool,
"source": volMigrator.customVolumeArgs.Name,
}
if volMigrator.migrationType == MigrationTypeVolumeFilesystem {
mountPath, err := m.asker.AskString("Provide mount path for this disk: ", "", nil)
if err != nil {
return err
}
m.instanceArgs.Devices[volMigrator.customVolumeArgs.Name]["path"] = mountPath
}
m.volumes = append(m.volumes, volMigrator)
return nil
}
func (m *InstanceMigration) askUEFISupport() error {
if m.instanceArgs.Type == api.InstanceTypeVM {
architectureName, _ := osarch.ArchitectureGetLocal()
if slices.Contains([]string{"x86_64", "aarch64"}, architectureName) {
hasUEFI, err := m.asker.AskBool("Does the VM support UEFI booting? [default=yes]: ", "yes")
if err != nil {
return err
}
if hasUEFI {
hasSecureBoot, err := m.asker.AskBool("Does the VM support UEFI Secure Boot? [default=yes]: ", "yes")
if err != nil {
return err
}
if !hasSecureBoot {
m.instanceArgs.Config["security.secureboot"] = "false"
}
} else {
m.instanceArgs.Config["security.csm"] = "true"
m.instanceArgs.Config["security.secureboot"] = "false"
}
}
}
return nil
}