2018-02-09 21:27:00 -05:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
2022-01-10 17:25:52 +01:00
|
|
|
"bufio"
|
|
|
|
|
"context"
|
2022-03-01 17:00:42 +01:00
|
|
|
"errors"
|
2018-02-09 21:27:00 -05:00
|
|
|
"fmt"
|
2026-02-01 11:55:19 +01:00
|
|
|
"io"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/url"
|
2018-02-09 21:27:00 -05:00
|
|
|
"os"
|
|
|
|
|
"os/exec"
|
2022-01-10 17:25:52 +01:00
|
|
|
"os/signal"
|
|
|
|
|
"path/filepath"
|
2020-05-19 17:28:03 -04:00
|
|
|
"runtime"
|
2024-02-23 22:04:15 +01:00
|
|
|
"slices"
|
2018-02-09 21:27:00 -05:00
|
|
|
"sort"
|
|
|
|
|
|
|
|
|
|
"github.com/spf13/cobra"
|
2019-06-03 20:14:24 +02:00
|
|
|
"golang.org/x/sys/unix"
|
2018-02-09 21:27:00 -05:00
|
|
|
|
2024-11-19 16:41:29 +00:00
|
|
|
incus "github.com/lxc/incus/v6/client"
|
2024-09-11 12:01:42 -06:00
|
|
|
"github.com/lxc/incus/v6/internal/linux"
|
2024-04-05 11:11:08 -04:00
|
|
|
"github.com/lxc/incus/v6/internal/version"
|
|
|
|
|
"github.com/lxc/incus/v6/shared/api"
|
2024-09-11 12:01:42 -06:00
|
|
|
"github.com/lxc/incus/v6/shared/archive"
|
2025-05-01 23:18:30 +02:00
|
|
|
"github.com/lxc/incus/v6/shared/ask"
|
2024-04-05 11:11:08 -04:00
|
|
|
localtls "github.com/lxc/incus/v6/shared/tls"
|
|
|
|
|
"github.com/lxc/incus/v6/shared/util"
|
2018-02-09 21:27:00 -05:00
|
|
|
)
|
|
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
// Migrator defines the methods required to perform a migration.
|
|
|
|
|
type Migrator interface {
|
|
|
|
|
gatherInfo() error
|
|
|
|
|
migrate() error
|
|
|
|
|
renderObject() error
|
|
|
|
|
}
|
2018-02-09 21:27:00 -05:00
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
// Migration is a base representation of a migration, which can be extended by more specific structs.
|
|
|
|
|
type Migration struct {
|
|
|
|
|
asker ask.Asker
|
|
|
|
|
ctx context.Context
|
|
|
|
|
migrationType MigrationType
|
|
|
|
|
mounts []string
|
|
|
|
|
pool string
|
|
|
|
|
project string
|
|
|
|
|
server incus.InstanceServer
|
|
|
|
|
sourceFormat string
|
|
|
|
|
sourcePath string
|
2025-05-18 22:43:19 -04:00
|
|
|
target string
|
2018-02-09 21:27:00 -05:00
|
|
|
}
|
|
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
func (m *Migration) runMigration(migrationHandler func(path string) error) error {
|
|
|
|
|
// Create the temporary directory to be used for the mounts
|
|
|
|
|
path, err := os.MkdirTemp("", "incus-migrate_mount_")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-01 10:53:29 +01:00
|
|
|
var fullPath string
|
2025-05-01 23:18:30 +02:00
|
|
|
|
2026-02-01 10:53:29 +01:00
|
|
|
if m.migrationType == MigrationTypeContainer || m.migrationType == MigrationTypeVolumeFilesystem {
|
|
|
|
|
m.mounts = append(m.mounts, m.sourcePath)
|
2025-05-01 23:18:30 +02:00
|
|
|
|
2026-02-01 10:53:29 +01:00
|
|
|
// Get and sort the mounts.
|
|
|
|
|
sort.Strings(m.mounts)
|
2025-05-01 23:18:30 +02:00
|
|
|
|
2026-02-01 10:53:29 +01:00
|
|
|
// Ensure we're not moved around.
|
|
|
|
|
runtime.LockOSThread()
|
|
|
|
|
defer runtime.UnlockOSThread()
|
|
|
|
|
|
|
|
|
|
// Unshare a new mntns so our mounts don't leak.
|
|
|
|
|
err := unix.Unshare(unix.CLONE_NEWNS)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Failed to unshare mount namespace: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Prevent mount propagation back to initial namespace
|
|
|
|
|
err = unix.Mount("", "/", "", unix.MS_REC|unix.MS_PRIVATE, "")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Failed to disable mount propagation: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Automatically clean-up the temporary path on exit
|
|
|
|
|
defer func(path string) {
|
|
|
|
|
// Unmount the path if it's a mountpoint.
|
|
|
|
|
_ = unix.Unmount(path, unix.MNT_DETACH)
|
|
|
|
|
_ = unix.Unmount(filepath.Join(path, "root.img"), unix.MNT_DETACH)
|
|
|
|
|
|
|
|
|
|
// Cleanup VM image files.
|
|
|
|
|
_ = os.Remove(filepath.Join(path, "converted-raw-image.img"))
|
|
|
|
|
_ = os.Remove(filepath.Join(path, "root.img"))
|
|
|
|
|
|
|
|
|
|
// Remove the directory itself.
|
|
|
|
|
_ = os.Remove(path)
|
|
|
|
|
}(path)
|
2025-05-01 23:18:30 +02:00
|
|
|
|
|
|
|
|
// Create the rootfs directory
|
|
|
|
|
fullPath = fmt.Sprintf("%s/rootfs", path)
|
|
|
|
|
|
|
|
|
|
err = os.Mkdir(fullPath, 0o755)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Setup the source (mounts)
|
|
|
|
|
err = setupSource(fullPath, m.mounts)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Failed to setup the source: %w", err)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
_, ext, convCmd, _ := archive.DetectCompression(m.sourcePath)
|
|
|
|
|
if ext == ".qcow2" || ext == ".vmdk" {
|
|
|
|
|
// COnfirm the command is available.
|
|
|
|
|
_, err := exec.LookPath(convCmd[0])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Unable to find required command %q", convCmd[0])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
destImg := filepath.Join(path, "converted-raw-image.img")
|
|
|
|
|
|
|
|
|
|
cmd := []string{
|
|
|
|
|
"nice", "-n19", // Run with low priority to reduce CPU impact on other processes.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd = append(cmd, convCmd...)
|
|
|
|
|
cmd = append(cmd, "-p", "-t", "writeback")
|
|
|
|
|
|
|
|
|
|
// Check for Direct I/O support.
|
|
|
|
|
from, err := os.OpenFile(m.sourcePath, unix.O_DIRECT|unix.O_RDONLY, 0)
|
|
|
|
|
if err == nil {
|
|
|
|
|
cmd = append(cmd, "-T", "none")
|
|
|
|
|
_ = from.Close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
to, err := os.OpenFile(destImg, unix.O_DIRECT|unix.O_RDONLY, 0)
|
|
|
|
|
if err == nil {
|
|
|
|
|
cmd = append(cmd, "-t", "none")
|
|
|
|
|
_ = to.Close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd = append(cmd, m.sourcePath, destImg)
|
|
|
|
|
|
|
|
|
|
fmt.Printf("Converting image %q to raw format before importing\n", m.sourcePath)
|
|
|
|
|
|
|
|
|
|
c := exec.Command(cmd[0], cmd[1:]...)
|
|
|
|
|
err = c.Run()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Failed to convert image %q for importing: %w", m.sourcePath, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m.sourcePath = destImg
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-01 10:53:29 +01:00
|
|
|
err = os.Symlink(m.sourcePath, filepath.Join(path, "root.img"))
|
2025-05-01 23:18:30 +02:00
|
|
|
if err != nil {
|
2026-02-01 10:53:29 +01:00
|
|
|
return err
|
2025-05-01 23:18:30 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-01 10:53:29 +01:00
|
|
|
fullPath = path
|
2025-05-01 23:18:30 +02:00
|
|
|
}
|
2018-02-23 21:02:48 -05:00
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
return migrationHandler(fullPath)
|
2022-01-10 17:25:52 +01:00
|
|
|
}
|
|
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
func (m *Migration) setSourceFormat() error {
|
|
|
|
|
if m.sourcePath == "" {
|
2025-05-23 01:27:26 -04:00
|
|
|
return errors.New("Missing source path")
|
2022-01-10 17:25:52 +01:00
|
|
|
}
|
|
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
if m.migrationType == "" {
|
2025-05-23 01:27:26 -04:00
|
|
|
return errors.New("Missing migration type")
|
2025-05-01 23:18:30 +02:00
|
|
|
}
|
2022-01-10 17:25:52 +01:00
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
// When migrating a disk, report the detected source format
|
|
|
|
|
if m.migrationType == MigrationTypeVM || m.migrationType == MigrationTypeVolumeBlock {
|
|
|
|
|
if linux.IsBlockdevPath(m.sourcePath) {
|
|
|
|
|
m.sourceFormat = "Block device"
|
|
|
|
|
} else if _, ext, _, _ := archive.DetectCompression(m.sourcePath); ext == ".qcow2" {
|
|
|
|
|
m.sourceFormat = "qcow2"
|
|
|
|
|
} else if _, ext, _, _ := archive.DetectCompression(m.sourcePath); ext == ".vmdk" {
|
|
|
|
|
m.sourceFormat = "vmdk"
|
|
|
|
|
} else {
|
|
|
|
|
// If the input isn't a block device or qcow2/vmdk image, assume it's raw.
|
|
|
|
|
// Positively identifying a raw image depends on parsing MBR/GPT partition tables.
|
|
|
|
|
m.sourceFormat = "raw"
|
2022-01-10 17:25:52 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-18 22:43:19 -04:00
|
|
|
func (m *Migration) askTarget() error {
|
|
|
|
|
if !m.server.IsClustered() {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ok, err := m.asker.AskBool("Would you like to target a specific server or group in the cluster? [default=no]: ", "no")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
clusterTarget, err := m.asker.AskString("Target name: ", "", nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m.target = clusterTarget
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
func (m *Migration) askSourcePath(question string) error {
|
|
|
|
|
var err error
|
|
|
|
|
|
2026-02-01 11:55:19 +01:00
|
|
|
var isURL bool
|
2025-05-01 23:18:30 +02:00
|
|
|
m.sourcePath, err = m.asker.AskString(question, "", func(s string) error {
|
2026-02-01 11:55:19 +01:00
|
|
|
// Allow URLs.
|
|
|
|
|
isURL = false
|
|
|
|
|
|
|
|
|
|
_, err := url.Parse(s)
|
|
|
|
|
if err == nil {
|
|
|
|
|
isURL = true
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if a valid path.
|
2025-05-01 23:18:30 +02:00
|
|
|
if !util.PathExists(s) {
|
|
|
|
|
return errors.New("Path does not exist")
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-01 11:55:19 +01:00
|
|
|
_, err = os.Stat(s)
|
2025-05-01 23:18:30 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2022-01-10 17:25:52 +01:00
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
return nil
|
|
|
|
|
})
|
2022-01-10 17:25:52 +01:00
|
|
|
if err != nil {
|
2025-05-01 23:18:30 +02:00
|
|
|
return err
|
2022-01-10 17:25:52 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-01 11:55:19 +01:00
|
|
|
// If a URL, download it.
|
|
|
|
|
if isURL {
|
|
|
|
|
// Create a temporary file.
|
|
|
|
|
f, err := os.CreateTemp("", "")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defer func() { _ = f.Close() }()
|
|
|
|
|
|
|
|
|
|
// Download the target.
|
|
|
|
|
resp, err := http.Get(m.sourcePath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
fmt.Printf("Downloading %q\n", m.sourcePath)
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
// Read 4MB at a time.
|
|
|
|
|
_, err = io.CopyN(f, resp.Body, 4*1024*1024)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err == io.EOF {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_ = os.Remove(f.Name())
|
|
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m.sourcePath = f.Name()
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
return nil
|
2022-01-10 17:25:52 +01:00
|
|
|
}
|
|
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
func (m *Migration) askProject(question string) error {
|
|
|
|
|
projectNames, err := m.server.GetProjectNames()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
2025-04-28 17:37:54 +02:00
|
|
|
}
|
|
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
if len(projectNames) > 1 {
|
|
|
|
|
project, err := m.asker.AskChoice(question, projectNames, api.ProjectDefaultName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m.project = project
|
|
|
|
|
return nil
|
2025-04-28 17:37:54 +02:00
|
|
|
}
|
|
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
m.project = api.ProjectDefaultName
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type cmdMigrate struct {
|
|
|
|
|
global *cmdGlobal
|
|
|
|
|
|
|
|
|
|
flagRsyncArgs string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *cmdMigrate) command() *cobra.Command {
|
|
|
|
|
cmd := &cobra.Command{}
|
|
|
|
|
cmd.Use = "incus-migrate"
|
|
|
|
|
cmd.Short = "Physical to instance migration tool"
|
|
|
|
|
cmd.Long = `Description:
|
|
|
|
|
Physical to instance migration tool
|
|
|
|
|
|
|
|
|
|
This tool lets you turn any Linux filesystem (including your current one)
|
|
|
|
|
into an instance on a remote host.
|
|
|
|
|
|
|
|
|
|
It will setup a clean mount tree made of the root filesystem and any
|
|
|
|
|
additional mount you list, then transfer this through the migration
|
|
|
|
|
API to create a new instance from it.
|
|
|
|
|
|
|
|
|
|
The same set of options as ` + "`incus launch`" + ` are also supported.
|
|
|
|
|
`
|
|
|
|
|
cmd.RunE = c.run
|
|
|
|
|
cmd.Flags().StringVar(&c.flagRsyncArgs, "rsync-args", "", "Extra arguments to pass to rsync (for file transfers)"+"``")
|
|
|
|
|
|
|
|
|
|
return cmd
|
2025-04-28 17:37:54 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-01 07:27:58 +10:00
|
|
|
func (c *cmdMigrate) askServer() (incus.InstanceServer, string, error) {
|
2024-03-25 13:55:18 -04:00
|
|
|
// Detect local server.
|
|
|
|
|
local, err := c.connectLocal()
|
|
|
|
|
if err == nil {
|
|
|
|
|
useLocal, err := c.global.asker.AskBool("The local Incus server is the target [default=yes]: ", "yes")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if useLocal {
|
|
|
|
|
return local, "", nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-10 17:25:52 +01:00
|
|
|
// Server address
|
2023-09-12 03:30:13 +00:00
|
|
|
serverURL, err := c.global.asker.AskString("Please provide Incus server URL: ", "", nil)
|
2022-01-10 17:25:52 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serverURL, err = parseURL(serverURL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, "", err
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-01 07:27:58 +10:00
|
|
|
args := incus.ConnectionArgs{
|
2024-05-22 13:40:52 -04:00
|
|
|
UserAgent: fmt.Sprintf("LXC-MIGRATE %s", version.Version),
|
2022-01-10 17:25:52 +01:00
|
|
|
}
|
|
|
|
|
|
2024-05-22 13:40:52 -04:00
|
|
|
// Attempt to connect
|
|
|
|
|
server, err := incus.ConnectIncus(serverURL, &args)
|
2022-01-10 17:25:52 +01:00
|
|
|
if err != nil {
|
2024-05-22 13:40:52 -04:00
|
|
|
// Failed to connect using the system CA, so retrieve the remote certificate.
|
|
|
|
|
certificate, err := localtls.GetRemoteCertificate(serverURL, args.UserAgent)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, "", fmt.Errorf("Failed to get remote certificate: %w", err)
|
|
|
|
|
}
|
2022-01-10 17:25:52 +01:00
|
|
|
|
2024-05-22 13:40:52 -04:00
|
|
|
digest := localtls.CertFingerprint(certificate)
|
2022-01-10 17:25:52 +01:00
|
|
|
|
2024-05-22 13:40:52 -04:00
|
|
|
fmt.Println("Certificate fingerprint:", digest)
|
|
|
|
|
fmt.Print("ok (y/n)? ")
|
2023-09-28 02:20:17 -04:00
|
|
|
|
2024-05-22 13:40:52 -04:00
|
|
|
buf := bufio.NewReader(os.Stdin)
|
|
|
|
|
line, _, err := buf.ReadLine()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, "", err
|
|
|
|
|
}
|
2022-01-10 17:25:52 +01:00
|
|
|
|
2024-05-22 13:40:52 -04:00
|
|
|
if len(line) < 1 || line[0] != 'y' && line[0] != 'Y' {
|
2025-05-23 01:27:26 -04:00
|
|
|
return nil, "", errors.New("Server certificate rejected by user")
|
2024-05-22 13:40:52 -04:00
|
|
|
}
|
2022-01-10 17:25:52 +01:00
|
|
|
|
2024-05-22 13:40:52 -04:00
|
|
|
args.InsecureSkipVerify = true
|
|
|
|
|
server, err = incus.ConnectIncus(serverURL, &args)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, "", fmt.Errorf("Failed to connect to server: %w", err)
|
|
|
|
|
}
|
2022-01-10 17:25:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
apiServer, _, err := server.GetServer()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, "", fmt.Errorf("Failed to get server: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Println("")
|
|
|
|
|
|
|
|
|
|
type AuthMethod int
|
|
|
|
|
|
|
|
|
|
const (
|
2023-08-12 21:20:56 +02:00
|
|
|
authMethodTLSCertificate AuthMethod = iota
|
2022-01-10 17:25:52 +01:00
|
|
|
authMethodTLSTemporaryCertificate
|
2022-02-11 10:22:34 +01:00
|
|
|
authMethodTLSCertificateToken
|
2022-01-10 17:25:52 +01:00
|
|
|
)
|
|
|
|
|
|
2023-08-30 00:06:00 -04:00
|
|
|
// TLS is always available
|
2022-01-10 17:25:52 +01:00
|
|
|
var availableAuthMethods []AuthMethod
|
|
|
|
|
var authMethod AuthMethod
|
|
|
|
|
|
|
|
|
|
i := 1
|
|
|
|
|
|
2024-02-23 22:04:15 +01:00
|
|
|
if slices.Contains(apiServer.AuthMethods, api.AuthenticationMethodTLS) {
|
2022-02-11 10:22:34 +01:00
|
|
|
fmt.Printf("%d) Use a certificate token\n", i)
|
|
|
|
|
availableAuthMethods = append(availableAuthMethods, authMethodTLSCertificateToken)
|
|
|
|
|
i++
|
2022-01-10 17:25:52 +01:00
|
|
|
fmt.Printf("%d) Use an existing TLS authentication certificate\n", i)
|
|
|
|
|
availableAuthMethods = append(availableAuthMethods, authMethodTLSCertificate)
|
|
|
|
|
i++
|
|
|
|
|
fmt.Printf("%d) Generate a temporary TLS authentication certificate\n", i)
|
|
|
|
|
availableAuthMethods = append(availableAuthMethods, authMethodTLSTemporaryCertificate)
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-23 22:04:15 +01:00
|
|
|
if len(apiServer.AuthMethods) > 1 || slices.Contains(apiServer.AuthMethods, api.AuthenticationMethodTLS) {
|
2023-09-12 03:30:13 +00:00
|
|
|
authMethodInt, err := c.global.asker.AskInt("Please pick an authentication mechanism above: ", 1, int64(i), "", nil)
|
2022-01-10 17:25:52 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
authMethod = availableAuthMethods[authMethodInt-1]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var certPath string
|
|
|
|
|
var keyPath string
|
2022-02-11 10:22:34 +01:00
|
|
|
var token string
|
2022-01-10 17:25:52 +01:00
|
|
|
|
2025-04-05 21:05:39 -04:00
|
|
|
switch authMethod {
|
|
|
|
|
case authMethodTLSCertificate:
|
2023-09-12 03:30:13 +00:00
|
|
|
certPath, err = c.global.asker.AskString("Please provide the certificate path: ", "", func(path string) error {
|
2023-09-28 02:20:17 -04:00
|
|
|
if !util.PathExists(path) {
|
2022-01-10 17:25:52 +01:00
|
|
|
return errors.New("File does not exist")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, "", err
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-12 03:30:13 +00:00
|
|
|
keyPath, err = c.global.asker.AskString("Please provide the keyfile path: ", "", func(path string) error {
|
2023-09-28 02:20:17 -04:00
|
|
|
if !util.PathExists(path) {
|
2022-01-10 17:25:52 +01:00
|
|
|
return errors.New("File does not exist")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, "", err
|
|
|
|
|
}
|
2025-04-05 21:05:39 -04:00
|
|
|
|
|
|
|
|
case authMethodTLSCertificateToken:
|
2023-09-12 03:30:13 +00:00
|
|
|
token, err = c.global.asker.AskString("Please provide the certificate token: ", "", func(token string) error {
|
2023-09-07 20:16:01 -04:00
|
|
|
_, err := localtls.CertificateTokenDecode(token)
|
2022-02-11 10:22:34 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, "", err
|
|
|
|
|
}
|
2025-04-30 18:27:57 +02:00
|
|
|
|
|
|
|
|
case authMethodTLSTemporaryCertificate:
|
|
|
|
|
// Intentionally ignored
|
2022-01-10 17:25:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var authType string
|
|
|
|
|
|
|
|
|
|
switch authMethod {
|
2022-02-11 10:22:34 +01:00
|
|
|
case authMethodTLSCertificate, authMethodTLSTemporaryCertificate, authMethodTLSCertificateToken:
|
2023-10-24 13:14:32 +01:00
|
|
|
authType = api.AuthenticationMethodTLS
|
2022-01-10 17:25:52 +01:00
|
|
|
}
|
|
|
|
|
|
2023-09-12 03:30:13 +00:00
|
|
|
return c.connectTarget(serverURL, certPath, keyPath, authType, token)
|
2022-01-10 17:25:52 +01:00
|
|
|
}
|
|
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
func (c *cmdMigrate) run(_ *cobra.Command, _ []string) error {
|
|
|
|
|
// Server
|
|
|
|
|
server, clientFingerprint, err := c.askServer()
|
2022-01-10 17:25:52 +01:00
|
|
|
if err != nil {
|
2025-05-01 23:18:30 +02:00
|
|
|
return err
|
2022-01-10 17:25:52 +01:00
|
|
|
}
|
|
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
sigChan := make(chan os.Signal, 1)
|
|
|
|
|
signal.Notify(sigChan, os.Interrupt)
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
2022-01-10 17:25:52 +01:00
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
go func() {
|
|
|
|
|
<-sigChan
|
2022-01-10 17:25:52 +01:00
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
if clientFingerprint != "" {
|
|
|
|
|
_ = server.DeleteCertificate(clientFingerprint)
|
2022-01-10 17:25:52 +01:00
|
|
|
}
|
|
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
cancel()
|
2022-01-10 17:25:52 +01:00
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
// The following nolint directive ignores the "deep-exit" rule of the revive linter.
|
|
|
|
|
// We should be exiting cleanly by passing the above context into each invoked method and checking for
|
|
|
|
|
// cancellation. Unfortunately our client methods do not accept a context argument.
|
|
|
|
|
os.Exit(1) //nolint:revive
|
|
|
|
|
}()
|
2022-01-10 17:25:52 +01:00
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
if clientFingerprint != "" {
|
|
|
|
|
defer func() { _ = server.DeleteCertificate(clientFingerprint) }()
|
2022-01-10 17:25:52 +01:00
|
|
|
}
|
|
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
// Provide migration type
|
|
|
|
|
creationType, err := c.global.asker.AskInt(`
|
|
|
|
|
What would you like to create?
|
|
|
|
|
1) Container
|
|
|
|
|
2) Virtual Machine
|
2025-05-06 20:40:37 +02:00
|
|
|
3) Virtual Machine (from .ova)
|
|
|
|
|
4) Custom Volume
|
2022-01-10 17:25:52 +01:00
|
|
|
|
2025-05-06 20:40:37 +02:00
|
|
|
Please enter the number of your choice: `, 1, 4, "", nil)
|
2025-05-01 23:18:30 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
2022-01-10 17:25:52 +01:00
|
|
|
}
|
|
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
var migrator Migrator
|
|
|
|
|
switch creationType {
|
|
|
|
|
case 1:
|
|
|
|
|
migrator = NewInstanceMigration(ctx, server, c.global.asker, c.flagRsyncArgs, MigrationTypeContainer)
|
|
|
|
|
case 2:
|
|
|
|
|
migrator = NewInstanceMigration(ctx, server, c.global.asker, c.flagRsyncArgs, MigrationTypeVM)
|
|
|
|
|
case 3:
|
2025-05-06 20:40:37 +02:00
|
|
|
migrator = NewOVAMigration(ctx, server, c.global.asker, c.flagRsyncArgs)
|
|
|
|
|
case 4:
|
2025-05-01 23:18:30 +02:00
|
|
|
migrator = NewVolumeMigration(ctx, server, c.global.asker, c.flagRsyncArgs)
|
2018-02-09 21:27:00 -05:00
|
|
|
}
|
|
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
err = migrator.gatherInfo()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2022-01-10 17:25:52 +01:00
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
err = migrator.renderObject()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2022-01-10 17:25:52 +01:00
|
|
|
|
2025-05-01 23:18:30 +02:00
|
|
|
return migrator.migrate()
|
2025-04-28 17:29:37 +02:00
|
|
|
}
|