Remove lxd-to-incus

Signed-off-by: Stéphane Graber <stgraber@stgraber.org>
This commit is contained in:
Stéphane Graber 2026-06-04 14:23:01 -04:00
parent 4596075585
commit fe296b7400
No known key found for this signature in database
GPG Key ID: C638974D64792D67
25 changed files with 0 additions and 2491 deletions

View File

@ -569,14 +569,6 @@ jobs:
GOARCH=amd64 go build -o bin/incus-migrate.x86_64 ./cmd/incus-migrate
GOARCH=arm64 go build -o bin/incus-migrate.aarch64 ./cmd/incus-migrate
- name: Build static lxd-to-incus
if: runner.os == 'Linux'
env:
CGO_ENABLED: 0
run: |
GOARCH=amd64 go build -o bin/lxd-to-incus.x86_64 ./cmd/lxd-to-incus
GOARCH=arm64 go build -o bin/lxd-to-incus.aarch64 ./cmd/lxd-to-incus
- name: Unit tests (client)
env:
CGO_ENABLED: 0

1
.gitignore vendored
View File

@ -10,7 +10,6 @@ tags
cmd/fuidshift/fuidshift
cmd/incus/incus
cmd/lxc-to-incus/lxc-to-incus
cmd/lxd-to-incus/lxd-to-incus
cmd/incus-agent/incus-agent
cmd/incus-benchmark/incus-benchmark
cmd/incus-migrate/incus-migrate

View File

@ -66,19 +66,6 @@ archives:
{{- else if eq .Arch "arm64" }}aarch64
{{- else }}{{ .Arch }}{{ end }}
- id: lxd-to-incus
ids:
- lxd-to-incus
formats:
- binary
name_template: >-
bin.
{{- if eq .Os "darwin" }}macos
{{- else }}{{ .Os }}{{ end }}.lxd-to-incus.
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "arm64" }}aarch64
{{- else }}{{ .Arch }}{{ end }}
before:
hooks:
- go mod download
@ -152,16 +139,6 @@ builds:
- amd64
- arm64
- id: lxd-to-incus
main: ./cmd/lxd-to-incus
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- amd64
- arm64
changelog:
use: "github-native"
@ -195,7 +172,6 @@ sboms:
- incus-benchmark
- incus-migrate
- incus-simplestreams
- lxd-to-incus
- id: source
artifacts: source

View File

@ -25,8 +25,6 @@ Incus is a true open source community project, free of any [CLA](https://en.wiki
remains released under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0).
It's maintained by the same team of developers that first created LXD.
LXD users wishing to migrate to Incus can easily do so through a migration tool called [`lxd-to-incus`](https://linuxcontainers.org/incus/docs/main/howto/server_migrate_lxd/).
## Get started
See [Getting started](https://linuxcontainers.org/incus/docs/main/tutorial/first_steps/) in the Incus documentation for installation instructions and first steps.

View File

@ -1,135 +0,0 @@
package main
import (
"encoding/binary"
"fmt"
"io"
"os"
"path/filepath"
"github.com/pierrec/lz4/v4"
internalIO "github.com/lxc/incus/v7/internal/io"
internalUtil "github.com/lxc/incus/v7/internal/util"
"github.com/lxc/incus/v7/shared/util"
)
// Uncompress the raft snapshot files in the given database directory.
//
// A backup will be created and kept around in case of errors.
func migrateDatabase(dir string) error {
global := filepath.Join(dir, "global")
err := internalUtil.DirCopy(global, global+".bak")
if err != nil {
return fmt.Errorf("Failed to backup database directory %q: %w", global, err)
}
files, err := os.ReadDir(global)
if err != nil {
return fmt.Errorf("Failed to list database directory %q: %w", global, err)
}
for _, file := range files {
var timestamp uint64
var first uint64
var last uint64
if !file.Type().IsRegular() {
continue
}
n, err := fmt.Sscanf(file.Name(), "snapshot-%d-%d-%d\n", &timestamp, &first, &last)
if err != nil || n != 3 {
continue
}
filename := filepath.Join(global, file.Name())
err = lz4Uncompress(filename)
if err != nil {
return fmt.Errorf("Failed to uncompress snapshot %q: %w", filename, err)
}
}
return nil
}
// Uncompress the given file, preserving its mode and ownership.
//
// If the file is not lz4-compressed, this is a no-op.
func lz4Uncompress(zfilename string) error {
zr := lz4.NewReader(nil)
zfile, err := os.Open(zfilename)
if err != nil {
return fmt.Errorf("Failed to open file %q: %w", zfilename, err)
}
buf := make([]byte, 4)
n, err := zfile.Read(buf)
if err != nil {
return fmt.Errorf("Failed to read header file %q: %w", zfilename, err)
}
if n != 4 {
return fmt.Errorf("Read only %d bytes from %q", n, zfilename)
}
// Check the file magic, and return now if it's not an lz4 file.
magic := binary.LittleEndian.Uint32(buf)
if magic != 0x184D2204 {
zfile.Close()
return nil
}
off, err := zfile.Seek(0, 0)
if err != nil {
return fmt.Errorf("Failed to seek %q: %w", zfilename, err)
}
if off != 0 {
return fmt.Errorf("Seek %q to offset: %d", zfilename, off)
}
zinfo, err := zfile.Stat()
if err != nil {
return fmt.Errorf("Failed to get file info for %q: %w", zfilename, err)
}
// use the same mode for the output file
mode := zinfo.Mode()
_, uid, gid := internalIO.GetOwnerMode(zinfo)
filename := zfilename + ".uncompressed"
file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
if err != nil {
return fmt.Errorf("Failed to open file %q: %w", filename, err)
}
zr.Reset(zfile)
_, err = util.SafeCopy(file, zr)
if err != nil {
return fmt.Errorf("Failed to uncompress %q into %q: %w", zfilename, filename, err)
}
for _, c := range []io.Closer{zfile, file} {
err := c.Close()
if err != nil {
return fmt.Errorf("Failed to close file: %w", err)
}
}
err = os.Chown(filename, uid, gid)
if err != nil {
return fmt.Errorf("Failed to set ownership of %q: %w", filename, err)
}
err = os.Rename(filename, zfilename)
if err != nil {
return fmt.Errorf("Failed to rename %q to %q: %w", filename, zfilename, err)
}
return nil
}

View File

@ -1,951 +0,0 @@
package main
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"slices"
"strings"
"time"
"github.com/spf13/cobra"
"golang.org/x/sys/unix"
incus "github.com/lxc/incus/v7/client"
"github.com/lxc/incus/v7/internal/linux"
"github.com/lxc/incus/v7/internal/version"
"github.com/lxc/incus/v7/shared/api"
"github.com/lxc/incus/v7/shared/ask"
"github.com/lxc/incus/v7/shared/subprocess"
"github.com/lxc/incus/v7/shared/util"
)
type cmdGlobal struct {
asker ask.Asker
flagHelp bool
flagVersion bool
}
func main() {
// Setup command line parser.
migrateCmd := cmdMigrate{}
app := migrateCmd.command()
app.Use = "lxd-to-incus"
app.Short = "LXD to Incus migration tool"
app.Long = `Description:
LXD to Incus migration tool
This tool allows an existing LXD user to move all their data over to Incus.
`
app.SilenceUsage = true
app.CompletionOptions = cobra.CompletionOptions{DisableDefaultCmd: true}
// Global flags.
globalCmd := cmdGlobal{asker: ask.NewAsker(bufio.NewReader(os.Stdin))}
migrateCmd.global = globalCmd
app.PersistentFlags().BoolVar(&globalCmd.flagVersion, "version", false, "Print version number")
app.PersistentFlags().BoolVarP(&globalCmd.flagHelp, "help", "h", false, "Print help")
// Version handling.
app.SetVersionTemplate("{{.Version}}\n")
app.Version = version.Version
// Run the main command and handle errors.
err := app.Execute()
if err != nil {
os.Exit(1)
}
}
type cmdMigrate struct {
global cmdGlobal
flagYes bool
flagClusterMember bool
flagIgnoreVersionCheck bool
}
func (c *cmdMigrate) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = "lxd-to-incus"
cmd.RunE = c.run
cmd.PersistentFlags().BoolVar(&c.flagYes, "yes", false, "Migrate without prompting")
cmd.PersistentFlags().BoolVar(&c.flagClusterMember, "cluster-member", false, "Used internally for cluster migrations")
cmd.PersistentFlags().BoolVar(&c.flagIgnoreVersionCheck, "ignore-version-check", false, "Bypass source version check")
return cmd
}
func (c *cmdMigrate) run(app *cobra.Command, args []string) error {
var err error
var srcClient incus.InstanceServer
var targetClient incus.InstanceServer
// Confirm that we're root.
if os.Geteuid() != 0 {
return errors.New("This tool must be run as root")
}
// Create log file.
logFile, err := os.Create(fmt.Sprintf("/var/log/lxd-to-incus.%d.log", os.Getpid()))
if err != nil {
return fmt.Errorf("Failed to create log file: %w", err)
}
defer logFile.Close()
err = logFile.Chmod(0o600)
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to set permissions on log file: %w", err)
}
if c.flagClusterMember {
_, _ = logFile.WriteString("Running in cluster member mode\n")
}
// Iterate through potential sources.
fmt.Println("=> Looking for source server")
var source source
for _, candidate := range sources {
if !candidate.present() {
continue
}
source = candidate
break
}
if source == nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return errors.New("No source server could be found")
}
fmt.Printf("==> Detected: %s\n", source.name())
_, _ = fmt.Fprintf(logFile, "Source server: %s\n", source.name())
// Iterate through potential targets.
fmt.Println("=> Looking for target server")
var target target
for _, candidate := range targets {
if !candidate.present() {
continue
}
target = candidate
break
}
if target == nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return errors.New("No target server could be found")
}
fmt.Printf("==> Detected: %s\n", target.name())
_, _ = fmt.Fprintf(logFile, "Target server: %s\n", target.name())
// Connect to the servers.
clustered := c.flagClusterMember
if !c.flagClusterMember {
fmt.Println("=> Connecting to source server")
srcClient, err = source.connect()
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to connect to the source: %w", err)
}
// Look for API incompatibility (bool in /1.0 config).
resp, _, err := srcClient.RawQuery("GET", "/1.0", nil, "")
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to get source server info: %w", err)
}
type lxdServer struct {
Config map[string]any `json:"config"`
}
s := lxdServer{}
err = json.Unmarshal(resp.Metadata, &s)
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to parse source server config: %w", err)
}
badEntries := []string{}
for k, v := range s.Config {
_, ok := v.(string)
if !ok {
badEntries = append(badEntries, k)
}
}
if len(badEntries) > 0 {
fmt.Println("")
fmt.Println("The source server (LXD) has the following configuration keys that are incompatible with Incus:")
for _, k := range badEntries {
fmt.Printf(" - %s\n", k)
}
fmt.Println("")
fmt.Println("The present migration tool cannot properly connect to the LXD server with those configuration keys present.")
fmt.Println("Please unset those configuration keys through the `lxc config unset` command and retry `lxd-to-incus`.")
fmt.Println("")
_, _ = fmt.Fprintf(logFile, "ERROR: Bad config keys: %v\n", badEntries)
return errors.New("Unable to interact with the source server")
}
// Get the source server info.
srcServerInfo, _, err := srcClient.GetServer()
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to get source server info: %w", err)
}
clustered = srcServerInfo.Environment.ServerClustered
}
if clustered {
_, _ = logFile.WriteString("Source server is a cluster\n")
}
fmt.Println("=> Connecting to the target server")
targetClient, err = target.connect()
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to connect to the target: %w", err)
}
// Configuration validation.
if !c.flagClusterMember {
err = c.validate(source, target)
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return err
}
}
// Grab the path information.
sourcePaths, err := source.paths()
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to get source paths: %w", err)
}
_, _ = fmt.Fprintf(logFile, "Source server paths: %+v\n", sourcePaths)
targetPaths, err := target.paths()
if err != nil {
return fmt.Errorf("Failed to get target paths: %w", err)
}
_, _ = fmt.Fprintf(logFile, "Target server paths: %+v\n", targetPaths)
// Mangle storage pool sources.
rewriteStatements := []string{}
rewriteCommands := [][]string{}
if !c.flagClusterMember {
var storagePools []api.StoragePool
if !clustered {
storagePools, err = srcClient.GetStoragePools()
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Couldn't list storage pools: %w", err)
}
} else {
clusterMembers, err := srcClient.GetClusterMembers()
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return errors.New("Failed to retrieve the list of cluster members")
}
for _, member := range clusterMembers {
poolNames, err := srcClient.UseTarget(member.ServerName).GetStoragePoolNames()
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Couldn't list storage pools: %w", err)
}
for _, poolName := range poolNames {
pool, _, err := srcClient.UseTarget(member.ServerName).GetStoragePool(poolName)
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Couldn't get storage pool: %w", err)
}
storagePools = append(storagePools, *pool)
}
}
}
rbdRenamed := []string{}
for _, pool := range storagePools {
if pool.Driver == "ceph" {
cephCluster, ok := pool.Config["ceph.cluster_name"]
if !ok {
cephCluster = "ceph"
}
cephUser, ok := pool.Config["ceph.user.name"]
if !ok {
cephUser = "admin"
}
cephPool, ok := pool.Config["ceph.osd.pool_name"]
if !ok {
cephPool = pool.Name
}
renameCmd := []string{"rbd", "rename", "--cluster", cephCluster, "--name", fmt.Sprintf("client.%s", cephUser), fmt.Sprintf("%s/lxd_%s", cephPool, cephPool), fmt.Sprintf("%s/incus_%s", cephPool, cephPool)}
if !slices.Contains(rbdRenamed, pool.Name) {
rewriteCommands = append(rewriteCommands, renameCmd)
rbdRenamed = append(rbdRenamed, pool.Name)
}
}
source := pool.Config["source"]
if source == "" || source[0] != byte('/') {
continue
}
if !strings.HasPrefix(source, sourcePaths.daemon) {
continue
}
newSource := strings.Replace(source, sourcePaths.daemon, targetPaths.daemon, 1)
rewriteStatements = append(rewriteStatements, fmt.Sprintf("UPDATE storage_pools_config SET value='%s' WHERE value='%s';", newSource, source))
}
}
// Mangle OVN.
if !c.flagClusterMember {
srcServerInfo, _, err := srcClient.GetServer()
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to get source server info: %w", err)
}
ovnNB, ok := srcServerInfo.Config["network.ovn.northbound_connection"]
if !ok && util.PathExists("/run/ovn/ovnnb_db.sock") {
ovnNB = "unix:/run/ovn/ovnnb_db.sock"
}
if ovnNB != "" {
if !c.flagClusterMember {
out, err := subprocess.RunCommand("ovs-vsctl", "get", "open_vswitch", ".", "external_ids:ovn-remote")
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to get OVN southbound database address: %w", err)
}
ovnSB := strings.TrimSpace(strings.ReplaceAll(out, "\"", ""))
commands, err := ovnConvert(ovnNB, ovnSB)
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to prepare OVN conversion: %v", err)
}
rewriteCommands = append(rewriteCommands, commands...)
err = ovnBackup(ovnNB, ovnSB, "/var/backups/")
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to backup the OVN database: %v", err)
}
}
}
}
// General database updates.
if !c.flagClusterMember {
// Mangle profile and project descriptions.
rewriteStatements = append(rewriteStatements, "UPDATE profiles SET description='Default Incus profile' WHERE description='Default LXD profile';")
rewriteStatements = append(rewriteStatements, "UPDATE projects SET description='Default Incus project' WHERE description='Default LXD project';")
// Remove volatile.uuid key from storage volumes (not used by Incus).
rewriteStatements = append(rewriteStatements, "DELETE FROM storage_volumes_config WHERE key='volatile.uuid';")
rewriteStatements = append(rewriteStatements, "DELETE FROM storage_volumes_snapshots_config WHERE key='volatile.uuid';")
// Remove volatile.uuid key from instances (not used by Incus).
rewriteStatements = append(rewriteStatements, "DELETE FROM instances_config WHERE key='volatile.uuid';")
rewriteStatements = append(rewriteStatements, "DELETE FROM instances_snapshots_config WHERE key='volatile.uuid';")
}
// Mangle database schema to be compatible.
if !c.flagClusterMember {
srcServerInfo, _, err := srcClient.GetServer()
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to get source server info: %w", err)
}
srcVersion, err := version.Parse(srcServerInfo.Environment.ServerVersion)
if err != nil {
return fmt.Errorf("Couldn't parse source server version: %w", err)
}
lxdVersionAuth := &version.DottedVersion{Major: 5, Minor: 21, Patch: 0}
if srcVersion.Compare(lxdVersionAuth) >= 0 {
// Re-create the certificate tables.
rewriteStatements = append(rewriteStatements, `CREATE TABLE certificates (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
fingerprint TEXT NOT NULL,
type INTEGER NOT NULL,
name TEXT NOT NULL,
certificate TEXT NOT NULL,
restricted INTEGER NOT NULL DEFAULT 0,
UNIQUE (fingerprint)
);
CREATE TABLE "certificates_projects" (
certificate_id INTEGER NOT NULL,
project_id INTEGER NOT NULL,
FOREIGN KEY (certificate_id) REFERENCES certificates (id) ON DELETE CASCADE,
FOREIGN KEY (project_id) REFERENCES "projects" (id) ON DELETE CASCADE,
UNIQUE (certificate_id, project_id)
);`)
// Revert the schema version.
rewriteStatements = append(rewriteStatements, `DELETE FROM schema WHERE version < 73;
UPDATE schema SET version=69 WHERE version=73;`)
// Convert back the entries.
rewriteStatements = append(rewriteStatements, `INSERT INTO certificates (id, fingerprint, type, name, certificate, restricted) SELECT id, identifier, 1, name, json_extract(metadata, "$.cert"), 1 FROM identities WHERE type=1;
INSERT INTO certificates (id, fingerprint, type, name, certificate, restricted) SELECT id, identifier, 1, name, json_extract(metadata, "$.cert"), 0 FROM identities WHERE type=2;
INSERT INTO certificates (id, fingerprint, type, name, certificate, restricted) SELECT id, identifier, 2, name, json_extract(metadata, "$.cert"), 0 FROM identities WHERE type=3;
INSERT INTO certificates (id, fingerprint, type, name, certificate, restricted) SELECT id, identifier, 3, name, json_extract(metadata, "$.cert"), 1 FROM identities WHERE type=4;
INSERT INTO certificates (id, fingerprint, type, name, certificate, restricted) SELECT id, identifier, 3, name, json_extract(metadata, "$.cert"), 0 FROM identities WHERE type=6;
INSERT INTO certificates_projects (certificate_id, project_id) SELECT identity_id, project_id FROM identities_projects;`)
// Drop the other tables.
rewriteStatements = append(rewriteStatements, `DROP TRIGGER IF EXISTS on_auth_group_delete;
DROP TRIGGER IF EXISTS on_cluster_group_delete;
DROP TRIGGER IF EXISTS on_identity_delete;
DROP TRIGGER IF EXISTS on_identity_provider_group_delete;
DROP TRIGGER IF EXISTS on_image_alias_delete;
DROP TRIGGER IF EXISTS on_image_delete;
DROP TRIGGER IF EXISTS on_instance_backup_delete;
DROP TRIGGER IF EXISTS on_instance_delete;
DROP TRIGGER IF EXISTS on_instance_snapshot_delete;
DROP TRIGGER IF EXISTS on_instance_snaphot_delete;
DROP TRIGGER IF EXISTS on_network_acl_delete;
DROP TRIGGER IF EXISTS on_network_delete;
DROP TRIGGER IF EXISTS on_network_zone_delete;
DROP TRIGGER IF EXISTS on_node_delete;
DROP TRIGGER IF EXISTS on_operation_delete;
DROP TRIGGER IF EXISTS on_profile_delete;
DROP TRIGGER IF EXISTS on_project_delete;
DROP TRIGGER IF EXISTS on_storage_bucket_delete;
DROP TRIGGER IF EXISTS on_storage_pool_delete;
DROP TRIGGER IF EXISTS on_storage_volume_backup_delete;
DROP TRIGGER IF EXISTS on_storage_volume_delete;
DROP TRIGGER IF EXISTS on_storage_volume_snapshot_delete;
DROP TRIGGER IF EXISTS on_warning_delete;
DROP TABLE IF EXISTS identities_projects;
DROP TABLE IF EXISTS auth_groups_permissions;
DROP TABLE IF EXISTS auth_groups_identity_provider_groups;
DROP TABLE IF EXISTS identities_auth_groups;
DROP TABLE IF EXISTS identity_provider_groups;
DROP TABLE IF EXISTS identities;
DROP TABLE IF EXISTS auth_groups;`)
}
}
// Log rewrite actions.
_, _ = logFile.WriteString("Rewrite SQL statements:\n")
for _, entry := range rewriteStatements {
_, _ = fmt.Fprintf(logFile, " - %s\n", entry)
}
_, _ = logFile.WriteString("Rewrite commands:\n")
for _, entry := range rewriteCommands {
_, _ = fmt.Fprintf(logFile, " - %s\n", strings.Join(entry, " "))
}
// Confirm migration.
if !c.flagClusterMember && !c.flagYes {
if !clustered {
fmt.Println(`
The migration is now ready to proceed.
At this point, the source server and all its instances will be stopped.
Instances will come back online once the migration is complete.`)
ok, err := c.global.asker.AskBool("Proceed with the migration? [default=no]: ", "no")
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return err
}
if !ok {
_, _ = logFile.WriteString("User aborted migration\n")
return errors.New("User aborted migration")
}
} else {
fmt.Println(`
The migration is now ready to proceed.
A cluster environment was detected.
Manual action will be needed on each of the server prior to Incus being functional.`)
if os.Getenv("CLUSTER_NO_STOP") != "1" {
fmt.Println("The migration will begin by shutting down instances on all servers.")
}
fmt.Println(`
It will then convert the current server over to Incus and then wait for the other servers to be converted.
Do not attempt to manually run this tool on any of the other servers in the cluster.
Instead this tool will be providing specific commands for each of the servers.`)
ok, err := c.global.asker.AskBool("Proceed with the migration? [default=no]: ", "no")
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return err
}
if !ok {
return errors.New("User aborted migration")
}
}
}
_, _ = logFile.WriteString("Migration started\n")
// Cluster evacuation.
if os.Getenv("CLUSTER_NO_STOP") == "1" {
_, _ = logFile.WriteString("WARN: User requested no instance stop during migration\n")
}
if !c.flagClusterMember && clustered && os.Getenv("CLUSTER_NO_STOP") != "1" {
fmt.Println("=> Stopping all workloads on the cluster")
clusterMembers, err := srcClient.GetClusterMembers()
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return errors.New("Failed to retrieve the list of cluster members")
}
for _, member := range clusterMembers {
fmt.Printf("==> Stopping all workloads on server %q\n", member.ServerName)
_, _ = fmt.Fprintf(logFile, "Stopping instances on server %qn\n", member.ServerName)
op, err := srcClient.UpdateClusterMemberState(member.ServerName, api.ClusterMemberStatePost{Action: "evacuate", Mode: "stop"})
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to stop workloads %q: %w", member.ServerName, err)
}
err = op.Wait()
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to stop workloads %q: %w", member.ServerName, err)
}
}
}
// Stop source.
fmt.Println("=> Stopping the source server")
_, _ = logFile.WriteString("Stopping the source server\n")
err = source.stop()
if err != nil {
return fmt.Errorf("Failed to stop the source server: %w", err)
}
// Stop target.
fmt.Println("=> Stopping the target server")
_, _ = logFile.WriteString("Stopping the target server\n")
err = target.stop()
if err != nil {
return fmt.Errorf("Failed to stop the target server: %w", err)
}
// Unmount potential mount points.
for _, mount := range []string{"devlxd", "shmounts"} {
_, _ = fmt.Fprintf(logFile, "Unmounting %q\n", filepath.Join(targetPaths.daemon, mount))
_ = unix.Unmount(filepath.Join(targetPaths.daemon, mount), unix.MNT_DETACH)
}
// Wipe the target.
fmt.Println("=> Wiping the target server")
_, _ = logFile.WriteString("Wiping the target server\n")
err = os.RemoveAll(targetPaths.logs)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to remove %q: %w", targetPaths.logs, err)
}
err = os.RemoveAll(targetPaths.cache)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to remove %q: %w", targetPaths.cache, err)
}
err = os.RemoveAll(targetPaths.daemon)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to remove %q: %w", targetPaths.daemon, err)
}
// Migrate data.
fmt.Println("=> Migrating the data")
_, _ = logFile.WriteString("Migrating the data\n")
_, err = subprocess.RunCommand("mv", sourcePaths.logs, targetPaths.logs)
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to move %q to %q: %w", sourcePaths.logs, targetPaths.logs, err)
}
_, err = subprocess.RunCommand("mv", sourcePaths.cache, targetPaths.cache)
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to move %q to %q: %w", sourcePaths.cache, targetPaths.cache, err)
}
if linux.IsMountPoint(sourcePaths.daemon) {
_, _ = logFile.WriteString("Source daemon path is a mountpoint\n")
err = os.MkdirAll(targetPaths.daemon, 0o711)
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to create target directory: %w", err)
}
_, _ = logFile.WriteString("Creating bind-mount of daemon path\n")
err = unix.Mount(sourcePaths.daemon, targetPaths.daemon, "none", unix.MS_BIND|unix.MS_REC, "")
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to bind mount %q to %q: %w", sourcePaths.daemon, targetPaths.daemon, err)
}
_, _ = logFile.WriteString("Unmounting former mountpoint\n")
err = unix.Unmount(sourcePaths.daemon, unix.MNT_DETACH)
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to unmount source mount %q: %w", sourcePaths.daemon, err)
}
fmt.Println("")
fmt.Printf("WARNING: %s was detected to be a mountpoint.\n", sourcePaths.daemon)
fmt.Printf("The migration logic has moved this mount to the new target path at %s.\n", targetPaths.daemon)
fmt.Print("However it is your responsibility to modify your system settings to ensure this mount will be properly restored on reboot.\n")
fmt.Println("")
} else {
_, _ = logFile.WriteString("Moving data over\n")
_, err = subprocess.RunCommand("mv", sourcePaths.daemon, targetPaths.daemon)
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to move %q to %q: %w", sourcePaths.daemon, targetPaths.daemon, err)
}
}
// Migrate database format.
fmt.Println("=> Migrating database")
_, _ = logFile.WriteString("Migrating database files\n")
_, err = subprocess.RunCommand("cp", "-R", filepath.Join(targetPaths.daemon, "database"), filepath.Join(targetPaths.daemon, "database.pre-migrate"))
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to backup the database: %w", err)
}
err = migrateDatabase(filepath.Join(targetPaths.daemon, "database"))
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to migrate database in %q: %w", filepath.Join(targetPaths.daemon, "database"), err)
}
// Apply custom migration statements.
if len(rewriteStatements) > 0 {
fmt.Println("=> Writing database patch")
_, _ = logFile.WriteString("Writing the database patch\n")
err = os.WriteFile(filepath.Join(targetPaths.daemon, "database", "patch.global.sql"), []byte(strings.Join(rewriteStatements, "\n")+"\n"), 0o600)
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to write database path: %w", err)
}
}
if len(rewriteCommands) > 0 {
fmt.Println("=> Running data migration commands")
_, _ = logFile.WriteString("Running data migration commands:\n")
failures := 0
for _, cmd := range rewriteCommands {
_, _ = fmt.Fprintf(logFile, " - %+v\n", cmd)
_, err := subprocess.RunCommand(cmd[0], cmd[1:]...)
if err != nil {
_, _ = fmt.Fprintf(logFile, "Failed to run command: %v\n", err)
failures++
}
}
if failures > 0 {
fmt.Printf("==> WARNING: %d commands out of %d succeeded (%d failures)\n", len(rewriteCommands)-failures, len(rewriteCommands), failures)
fmt.Println(" Please review the log file for details.")
fmt.Println(" Note that in OVN environments, it's normal to see some failures")
fmt.Println(" related to Flow Rules and Switch Ports as those often change during the migration.")
}
}
// Cleanup paths.
fmt.Println("=> Cleaning up target paths")
_, _ = logFile.WriteString("Cleaning up target paths\n")
for _, dir := range []string{"backups", "images"} {
_, _ = fmt.Fprintf(logFile, "Cleaning up path %q\n", filepath.Join(targetPaths.daemon, dir))
// Remove any potential symlink (ignore errors for real directories).
_ = os.Remove(filepath.Join(targetPaths.daemon, dir))
}
for _, dir := range []string{"devices", "devlxd", "security", "shmounts"} {
_, _ = fmt.Fprintf(logFile, "Cleaning up path %q\n", filepath.Join(targetPaths.daemon, dir))
_ = unix.Unmount(filepath.Join(targetPaths.daemon, dir), unix.MNT_DETACH)
err = os.RemoveAll(filepath.Join(targetPaths.daemon, dir))
if err != nil && !errors.Is(err, fs.ErrNotExist) {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to delete %q: %w", dir, err)
}
}
for _, dir := range []string{"containers", "containers-snapshots", "snapshots", "virtual-machines", "virtual-machines-snapshots"} {
entries, err := os.ReadDir(filepath.Join(targetPaths.daemon, dir))
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
continue
}
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to read entries in %q: %w", filepath.Join(targetPaths.daemon, dir), err)
}
_, _ = logFile.WriteString("Rewrite symlinks:\n")
for _, entry := range entries {
srcPath := filepath.Join(targetPaths.daemon, dir, entry.Name())
if entry.Type()&os.ModeSymlink != os.ModeSymlink {
continue
}
oldTarget, err := os.Readlink(srcPath)
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to resolve symlink %q: %w", srcPath, err)
}
newTarget := strings.Replace(oldTarget, sourcePaths.daemon, targetPaths.daemon, 1)
err = os.Remove(srcPath)
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to delete symlink %q: %w", srcPath, err)
}
_, _ = fmt.Fprintf(logFile, " - %q to %q\n", newTarget, srcPath)
err = os.Symlink(newTarget, srcPath)
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to create symlink %q: %w", srcPath, err)
}
}
}
// Cleanup the cache.
cacheEntries, err := os.ReadDir(targetPaths.cache)
if err == nil {
for _, entry := range cacheEntries {
if !entry.IsDir() {
continue
}
err := os.RemoveAll(filepath.Join(targetPaths.cache, entry.Name()))
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to clear cache file %q: %w", filepath.Join(targetPaths.cache, entry.Name()), err)
}
}
}
// Start target.
fmt.Println("=> Starting the target server")
_, _ = logFile.WriteString("Starting the target server\n")
err = target.start()
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to start the target server: %w", err)
}
// Cluster handling.
if clustered {
if !c.flagClusterMember {
_, _ = logFile.WriteString("Waiting for user to run command on other cluster members\n")
fmt.Println("=> Waiting for other cluster servers")
fmt.Println("")
fmt.Print("Please run `lxd-to-incus --cluster-member` on all other servers in the cluster\n\n")
for {
ok, err := c.global.asker.AskBool("The command has been started on all other servers? [default=no]: ", "no")
if !ok || err != nil {
continue
}
break
}
fmt.Println("")
_, _ = logFile.WriteString("User confirmed command was run on other members\n")
}
// Wait long enough that we get accurate heartbeat information.
fmt.Println("=> Waiting for cluster to be fully migrated")
_, _ = logFile.WriteString("Waiting for cluster to come back online\n")
time.Sleep(30 * time.Second)
for {
clusterMembers, err := targetClient.GetClusterMembers()
if err != nil {
time.Sleep(30 * time.Second)
continue
}
ready := true
for _, member := range clusterMembers {
info, _, err := targetClient.UseTarget(member.ServerName).GetServer()
if err != nil || info.Environment.Server != "incus" {
ready = false
break
}
if member.Status == "Evacuated" && member.Message == "Unavailable due to maintenance" {
continue
}
if member.Status == "Online" && member.Message == "Fully operational" {
continue
}
ready = false
break
}
if !ready {
time.Sleep(30 * time.Second)
continue
}
break
}
}
// Validate target.
fmt.Println("=> Checking the target server")
_, _ = logFile.WriteString("Checking target server\n")
targetServerInfo, _, err := targetClient.GetServer()
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to get target server info: %w", err)
}
// Fix OVS.
ovnNB, ok := targetServerInfo.Config["network.ovn.northbound_connection"]
if ok && ovnNB != "" {
commands, err := ovsConvert()
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to prepare OVS conversion: %v", err)
}
_, _ = logFile.WriteString("Running OVS conversion commands:\n")
for _, cmd := range commands {
_, _ = fmt.Fprintf(logFile, " - %+v\n", cmd)
_, err := subprocess.RunCommand(cmd[0], cmd[1:]...)
if err != nil {
_, _ = fmt.Fprintf(logFile, "Failed to run command: %v\n", err)
fmt.Fprintf(os.Stderr, "Failed to run command: %v\n", err)
}
}
}
// Cluster restore.
if !c.flagClusterMember && clustered {
fmt.Println("=> Restoring the cluster")
_, _ = logFile.WriteString("Restoring cluster state\n")
clusterMembers, err := targetClient.GetClusterMembers()
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return errors.New("Failed to retrieve the list of cluster members")
}
for _, member := range clusterMembers {
fmt.Printf("==> Restoring workloads on server %q\n", member.ServerName)
_, _ = fmt.Fprintf(logFile, "Restoring workloads on %q\n", member.ServerName)
op, err := targetClient.UpdateClusterMemberState(member.ServerName, api.ClusterMemberStatePost{Action: "restore"})
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to restore %q: %w", member.ServerName, err)
}
err = op.Wait()
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to restore %q: %w", member.ServerName, err)
}
}
}
// Writing completion stamp file.
completeFile, err := os.Create(filepath.Join(targetPaths.daemon, ".migrated-from-lxd"))
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
}
defer completeFile.Close()
// Confirm uninstall.
if !c.flagYes {
ok, err := c.global.asker.AskBool("Uninstall the LXD package? [default=no]: ", "no")
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return err
}
if !ok {
_, _ = logFile.WriteString("User decided not ro remove the package\n")
return nil
}
}
// Purge source.
fmt.Println("=> Uninstalling the source server")
_, _ = logFile.WriteString("Uninstalling the source package\n")
err = source.purge()
if err != nil {
_, _ = fmt.Fprintf(logFile, "ERROR: %v\n", err)
return fmt.Errorf("Failed to uninstall the source server: %w", err)
}
return nil
}

View File

@ -1,283 +0,0 @@
package main
import (
"context"
"encoding/csv"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/lxc/incus/v7/shared/subprocess"
)
func ovsConvert() ([][]string, error) {
commands := [][]string{}
output, err := subprocess.RunCommand("ovs-vsctl", "get", "open_vswitch", ".", "external-ids:ovn-bridge-mappings")
if err != nil {
// Assume that being unable to read the key means it's not set.
return nil, nil
}
oldValue := strings.TrimSpace(strings.ReplaceAll(output, "\"", ""))
oldBridges := []string{}
values := strings.Split(oldValue, ",")
for i, value := range values {
fields := strings.Split(value, ":")
oldBridges = append(oldBridges, fields[1])
fields[1] = strings.ReplaceAll(fields[1], "lxdovn", "incusovn")
values[i] = strings.Join(fields, ":")
}
newValue := strings.Join(values, ",")
if oldValue != newValue {
commands = append(commands, []string{"ovs-vsctl", "set", "open_vswitch", ".", fmt.Sprintf("external-ids:ovn-bridge-mappings=%s", newValue)})
}
for _, bridge := range oldBridges {
commands = append(commands, []string{"ovs-vsctl", "del-br", bridge})
}
return commands, nil
}
func ovnBackup(nbDB string, sbDB string, target string) error {
// Backup the Northbound database.
nbStdout, err := os.Create(filepath.Join(target, fmt.Sprintf("lxd-to-incus.ovn-nb.%d.backup", os.Getpid())))
if err != nil {
return err
}
defer nbStdout.Close()
err = nbStdout.Chmod(0o600)
if err != nil {
return err
}
args := []string{"dump", "-f", "csv", nbDB, "OVN_Northbound"}
if strings.Contains(nbDB, "ssl:") {
args = append(args, "-c", "/etc/ovn/cert_host")
args = append(args, "-p", "/etc/ovn/key_host")
args = append(args, "-C", "/etc/ovn/ovn-central.crt")
}
err = subprocess.RunCommandWithFds(context.Background(), nil, nbStdout, "ovsdb-client", args...)
if err != nil {
return err
}
// Backup the Southbound database.
sbStdout, err := os.Create(filepath.Join(target, fmt.Sprintf("lxd-to-incus.ovn-sb.%d.backup", os.Getpid())))
if err != nil {
return err
}
defer sbStdout.Close()
err = sbStdout.Chmod(0o600)
if err != nil {
return err
}
args = []string{"dump", "-f", "csv", sbDB, "OVN_Southbound"}
if strings.Contains(sbDB, "ssl:") {
args = append(args, "-c", "/etc/ovn/cert_host")
args = append(args, "-p", "/etc/ovn/key_host")
args = append(args, "-C", "/etc/ovn/ovn-central.crt")
}
err = subprocess.RunCommandWithFds(context.Background(), nil, sbStdout, "ovsdb-client", args...)
if err != nil {
return err
}
return nil
}
func ovnConvert(nbDB string, sbDB string) ([][]string, error) {
commands := [][]string{}
// Patch the Northbound records.
args := []string{"dump", "-f", "csv", nbDB, "OVN_Northbound"}
if strings.Contains(sbDB, "ssl:") {
args = append(args, "-c", "/etc/ovn/cert_host")
args = append(args, "-p", "/etc/ovn/key_host")
args = append(args, "-C", "/etc/ovn/ovn-central.crt")
}
output, err := subprocess.RunCommand("ovsdb-client", args...)
if err != nil {
return nil, err
}
data, err := ovnParseDump(output)
if err != nil {
return nil, err
}
for table, records := range data {
for _, record := range records {
for k, v := range record {
needsFixing, newValue, err := ovnCheckValue(table, k, v)
if err != nil {
return nil, err
}
if needsFixing {
cmd := []string{"ovn-nbctl", "--db", nbDB}
if strings.Contains(nbDB, "ssl:") {
cmd = append(cmd, "-c", "/etc/ovn/cert_host")
cmd = append(cmd, "-p", "/etc/ovn/key_host")
cmd = append(cmd, "-C", "/etc/ovn/ovn-central.crt")
}
cmd = append(cmd, []string{"set", table, record["_uuid"], fmt.Sprintf("%s=%s", k, newValue)}...)
commands = append(commands, cmd)
}
}
}
}
// Patch the Southbound records.
args = []string{"dump", "-f", "csv", sbDB, "OVN_Southbound"}
if strings.Contains(sbDB, "ssl:") {
args = append(args, "-c", "/etc/ovn/cert_host")
args = append(args, "-p", "/etc/ovn/key_host")
args = append(args, "-C", "/etc/ovn/ovn-central.crt")
}
output, err = subprocess.RunCommand("ovsdb-client", args...)
if err != nil {
return nil, err
}
data, err = ovnParseDump(output)
if err != nil {
return nil, err
}
for table, records := range data {
for _, record := range records {
for k, v := range record {
needsFixing, newValue, err := ovnCheckValue(table, k, v)
if err != nil {
return nil, err
}
if needsFixing {
cmd := []string{"ovn-sbctl", "--db", sbDB}
if strings.Contains(sbDB, "ssl:") {
cmd = append(cmd, "-c", "/etc/ovn/cert_host")
cmd = append(cmd, "-p", "/etc/ovn/key_host")
cmd = append(cmd, "-C", "/etc/ovn/ovn-central.crt")
}
cmd = append(cmd, []string{"set", table, record["_uuid"], fmt.Sprintf("%s=%s", k, newValue)}...)
commands = append(commands, cmd)
}
}
}
}
return commands, nil
}
func ovnCheckValue(table string, k string, v string) (bool, string, error) {
if !strings.Contains(v, "lxd") {
return false, "", nil
}
if table == "DNS" && k == "records" {
return false, "", nil
}
if table == "Chassis" && k == "other_config" {
return false, "", nil
}
if table == "Chassis" && k == "external_ids" {
return false, "", nil
}
if table == "Logical_Flow" && k == "actions" {
return false, "", nil
}
if table == "DHCP_Options" && k == "options" {
return false, "", nil
}
if table == "Logical_Router_Port" && k == "ipv6_ra_configs" {
return false, "", nil
}
if (table == "Logical_Switch_Port" || table == "Port_Binding") && k == "options" && (v == "{network_name=lxdbr0}" || v == "{network_name=lxdbr1}") {
return false, "", nil
}
newValue := strings.ReplaceAll(v, "lxd-net", "incus-net")
newValue = strings.ReplaceAll(newValue, "lxd_acl", "incus_acl")
newValue = strings.ReplaceAll(newValue, "lxd_location", "incus_location")
newValue = strings.ReplaceAll(newValue, "lxd_net", "incus_net")
newValue = strings.ReplaceAll(newValue, "lxd_port_group", "incus_port_group")
newValue = strings.ReplaceAll(newValue, "lxd_project_id", "incus_project_id")
newValue = strings.ReplaceAll(newValue, "lxd_switch", "incus_switch")
newValue = strings.ReplaceAll(newValue, "lxd_switch_port", "incus_switch_port")
if v == newValue {
return true, "", fmt.Errorf("Couldn't convert value %q for key %q in table %q", v, k, table)
}
return true, newValue, nil
}
func ovnParseDump(data string) (map[string][]map[string]string, error) {
output := map[string][]map[string]string{}
tableName := ""
fields := []string{}
newTable := false
for _, line := range strings.Split(data, "\n") {
if line == "" {
continue
}
if !strings.Contains(line, ",") && strings.HasSuffix(line, " table") {
newTable = true
tableName = strings.Split(line, " ")[0]
output[tableName] = []map[string]string{}
continue
}
if newTable {
newTable = false
var err error
fields, err = csv.NewReader(strings.NewReader(line)).Read()
if err != nil {
return nil, err
}
continue
}
record := map[string]string{}
entry, err := csv.NewReader(strings.NewReader(line)).Read()
if err != nil {
return nil, err
}
for k, v := range entry {
record[fields[k]] = v
}
output[tableName] = append(output[tableName], record)
}
return output, nil
}

View File

@ -1,7 +0,0 @@
package main
type daemonPaths struct {
daemon string
logs string
cache string
}

View File

@ -1,22 +0,0 @@
package main
import incus "github.com/lxc/incus/v7/client"
type source interface {
present() bool
stop() error
start() error
purge() error
connect() (incus.InstanceServer, error)
paths() (*daemonPaths, error)
name() string
}
var sources = []source{
&srcSnap{},
&srcDeb{},
&srcCOPR{},
&srcXbps{},
&srcAPK{},
&srcManual{},
}

View File

@ -1,49 +0,0 @@
package main
import (
incus "github.com/lxc/incus/v7/client"
"github.com/lxc/incus/v7/shared/subprocess"
"github.com/lxc/incus/v7/shared/util"
)
type srcAPK struct{}
func (s *srcAPK) present() bool {
if !util.PathExists("/var/lib/incus/") {
return false
}
_, err := subprocess.RunCommand("rc-service", "--exists", "incusd")
return err == nil
}
func (s *srcAPK) name() string {
return "apk package"
}
func (s *srcAPK) stop() error {
_, err := subprocess.RunCommand("rc-service", "lxd", "stop")
return err
}
func (s *srcAPK) start() error {
_, err := subprocess.RunCommand("rc-service", "lxd", "start")
return err
}
func (s *srcAPK) purge() error {
_, err := subprocess.RunCommand("apk", "del", "lxd", "lxd-client")
return err
}
func (s *srcAPK) connect() (incus.InstanceServer, error) {
return incus.ConnectIncusUnix("/var/lib/lxd/unix.socket", &incus.ConnectionArgs{SkipGetServer: true})
}
func (s *srcAPK) paths() (*daemonPaths, error) {
return &daemonPaths{
daemon: "/var/lib/lxd",
logs: "/var/log/lxd",
cache: "/var/cache/lxd",
}, nil
}

View File

@ -1,54 +0,0 @@
package main
import (
incus "github.com/lxc/incus/v7/client"
"github.com/lxc/incus/v7/shared/subprocess"
"github.com/lxc/incus/v7/shared/util"
)
type srcCOPR struct{}
func (s *srcCOPR) present() bool {
// Validate that the RPM package is installed.
_, err := subprocess.RunCommand("rpm", "-q", "lxd")
if err != nil {
return false
}
if !util.PathExists("/run/lxd.socket") {
return false
}
return true
}
func (s *srcCOPR) name() string {
return "COPR package"
}
func (s *srcCOPR) stop() error {
_, err := subprocess.RunCommand("systemctl", "stop", "lxd-containers.service", "lxd.service", "lxd.socket")
return err
}
func (s *srcCOPR) start() error {
_, err := subprocess.RunCommand("systemctl", "start", "lxd.socket", "lxd-containers.service")
return err
}
func (s *srcCOPR) purge() error {
_, err := subprocess.RunCommand("dnf", "remove", "-y", "lxd")
return err
}
func (s *srcCOPR) connect() (incus.InstanceServer, error) {
return incus.ConnectIncusUnix("/run/lxd.socket", &incus.ConnectionArgs{SkipGetServer: true})
}
func (s *srcCOPR) paths() (*daemonPaths, error) {
return &daemonPaths{
daemon: "/var/lib/lxd",
logs: "/var/log/lxd",
cache: "/var/cache/lxd",
}, nil
}

View File

@ -1,53 +0,0 @@
package main
import (
incus "github.com/lxc/incus/v7/client"
"github.com/lxc/incus/v7/shared/subprocess"
"github.com/lxc/incus/v7/shared/util"
)
type srcDeb struct{}
func (s *srcDeb) present() bool {
// Validate that the Debian package is installed.
if !util.PathExists("/var/lib/dpkg/info/lxd.list") {
return false
}
if !util.PathExists("/var/lib/lxd") {
return false
}
return true
}
func (s *srcDeb) name() string {
return ".deb package"
}
func (s *srcDeb) stop() error {
_, err := subprocess.RunCommand("systemctl", "stop", "lxd-containers.service", "lxd.service", "lxd.socket")
return err
}
func (s *srcDeb) start() error {
_, err := subprocess.RunCommand("systemctl", "start", "lxd.socket", "lxd-containers.service")
return err
}
func (s *srcDeb) purge() error {
_, err := subprocess.RunCommand("apt-get", "remove", "--yes", "--purge", "lxd", "lxd-client")
return err
}
func (s *srcDeb) connect() (incus.InstanceServer, error) {
return incus.ConnectIncusUnix("/var/lib/lxd/unix.socket", &incus.ConnectionArgs{SkipGetServer: true})
}
func (s *srcDeb) paths() (*daemonPaths, error) {
return &daemonPaths{
daemon: "/var/lib/lxd",
logs: "/var/log/lxd",
cache: "/var/cache/lxd",
}, nil
}

View File

@ -1,66 +0,0 @@
package main
import (
"errors"
"net/http"
"time"
incus "github.com/lxc/incus/v7/client"
"github.com/lxc/incus/v7/shared/util"
)
type srcManual struct{}
func (s *srcManual) present() bool {
return util.PathExists("/var/lib/lxd")
}
func (s *srcManual) name() string {
return "manual installation"
}
func (s *srcManual) stop() error {
d, err := s.connect()
if err != nil {
return err
}
httpClient, err := d.GetHTTPClient()
if err != nil {
return err
}
// Request shutdown, this shouldn't return until daemon has stopped so use a large request timeout.
httpTransport, ok := httpClient.Transport.(*http.Transport)
if !ok {
return errors.New("Bad transport type")
}
httpTransport.ResponseHeaderTimeout = 3600 * time.Second
_, _, err = d.RawQuery("PUT", "/internal/shutdown", nil, "")
if err != nil {
return err
}
return nil
}
func (s *srcManual) start() error {
return nil
}
func (s *srcManual) purge() error {
return nil
}
func (s *srcManual) connect() (incus.InstanceServer, error) {
return incus.ConnectIncusUnix("/var/lib/lxd/unix.socket", &incus.ConnectionArgs{SkipGetServer: true})
}
func (s *srcManual) paths() (*daemonPaths, error) {
return &daemonPaths{
daemon: "/var/lib/lxd",
logs: "/var/log/lxd",
cache: "/var/cache/lxd",
}, nil
}

View File

@ -1,53 +0,0 @@
package main
import (
incus "github.com/lxc/incus/v7/client"
"github.com/lxc/incus/v7/shared/subprocess"
"github.com/lxc/incus/v7/shared/util"
)
type srcSnap struct{}
func (s *srcSnap) present() bool {
// Validate that the snap is installed.
if !util.PathExists("/snap/lxd") && !util.PathExists("/var/lib/snapd/snap/lxd") {
return false
}
if !util.PathExists("/var/snap/lxd") {
return false
}
return true
}
func (s *srcSnap) name() string {
return "snap package"
}
func (s *srcSnap) stop() error {
_, err := subprocess.RunCommand("snap", "stop", "lxd")
return err
}
func (s *srcSnap) start() error {
_, err := subprocess.RunCommand("snap", "start", "lxd")
return err
}
func (s *srcSnap) purge() error {
_, err := subprocess.RunCommand("snap", "remove", "lxd", "--purge")
return err
}
func (s *srcSnap) connect() (incus.InstanceServer, error) {
return incus.ConnectIncusUnix("/var/snap/lxd/common/lxd/unix.socket", &incus.ConnectionArgs{SkipGetServer: true})
}
func (s *srcSnap) paths() (*daemonPaths, error) {
return &daemonPaths{
daemon: "/var/snap/lxd/common/lxd",
logs: "/var/snap/lxd/common/lxd/logs",
cache: "/var/snap/lxd/common/lxd/cache",
}, nil
}

View File

@ -1,56 +0,0 @@
package main
import (
incus "github.com/lxc/incus/v7/client"
"github.com/lxc/incus/v7/shared/subprocess"
"github.com/lxc/incus/v7/shared/util"
)
type srcXbps struct{}
func (s *srcXbps) present() bool {
if !util.PathExists("/var/db/xbps/.lxd-files.plist") {
return false
}
if !util.PathExists("/var/service/lxd") {
return false
}
if !util.PathExists("/var/lib/lxd/unix.socket") {
return false
}
return true
}
func (s *srcXbps) name() string {
return "xbps"
}
func (s *srcXbps) stop() error {
_, err := subprocess.RunCommand("sv", "stop", "lxd")
return err
}
func (s *srcXbps) start() error {
_, err := subprocess.RunCommand("sv", "start", "lxd")
return err
}
func (s *srcXbps) purge() error {
_, err := subprocess.RunCommand("xbps-remove", "-R", "-y", "lxd")
return err
}
func (s *srcXbps) connect() (incus.InstanceServer, error) {
return incus.ConnectIncusUnix("/var/lib/lxd/unix.socket", &incus.ConnectionArgs{SkipGetServer: true})
}
func (s *srcXbps) paths() (*daemonPaths, error) {
return &daemonPaths{
daemon: "/var/lib/lxd",
logs: "/var/log/lxd",
cache: "/var/cache/lxd",
}, nil
}

View File

@ -1,18 +0,0 @@
package main
import incus "github.com/lxc/incus/v7/client"
type target interface {
present() bool
stop() error
start() error
connect() (incus.InstanceServer, error)
paths() (*daemonPaths, error)
name() string
}
var targets = []target{
&targetSystemd{},
&targetOpenRC{},
&targetXbps{},
}

View File

@ -1,73 +0,0 @@
package main
import (
"time"
incus "github.com/lxc/incus/v7/client"
"github.com/lxc/incus/v7/shared/subprocess"
"github.com/lxc/incus/v7/shared/util"
)
type targetOpenRC struct {
service string
}
func (s *targetOpenRC) present() bool {
if !util.PathExists("/var/lib/incus/") {
return false
}
_, err := subprocess.RunCommand("rc-service", "--exists", "incus")
if err == nil {
s.service = "incus"
return true
}
_, err = subprocess.RunCommand("rc-service", "--exists", "incusd")
if err == nil {
s.service = "incusd"
return true
}
return false
}
func (s *targetOpenRC) stop() error {
_, err := subprocess.RunCommand("rc-service", s.service, "stop")
if err != nil {
return err
}
// Wait for the service to fully stop.
time.Sleep(5 * time.Second)
return nil
}
func (s *targetOpenRC) start() error {
_, err := subprocess.RunCommand("rc-service", s.service, "start")
if err != nil {
return err
}
// Wait for the socket to become available.
time.Sleep(5 * time.Second)
return nil
}
func (s *targetOpenRC) connect() (incus.InstanceServer, error) {
return incus.ConnectIncusUnix("/var/lib/incus/unix.socket", &incus.ConnectionArgs{SkipGetServer: true})
}
func (s *targetOpenRC) paths() (*daemonPaths, error) {
return &daemonPaths{
daemon: "/var/lib/incus",
logs: "/var/log/incus",
cache: "/var/cache/incus",
}, nil
}
func (s *targetOpenRC) name() string {
return "openrc"
}

View File

@ -1,57 +0,0 @@
package main
import (
"time"
incus "github.com/lxc/incus/v7/client"
"github.com/lxc/incus/v7/shared/subprocess"
"github.com/lxc/incus/v7/shared/util"
)
type targetSystemd struct{}
func (s *targetSystemd) present() bool {
if !util.PathExists("/var/lib/incus/") {
return false
}
_, err := subprocess.RunCommand("systemctl", "list-unit-files", "incus.service")
return err == nil
}
func (s *targetSystemd) stop() error {
_, err := subprocess.RunCommand("systemctl", "stop", "incus.service", "incus.socket")
return err
}
func (s *targetSystemd) start() error {
_, err := subprocess.RunCommand("systemctl", "start", "incus.service", "incus.socket")
if err != nil {
return err
}
// Wait for the socket to become available.
time.Sleep(5 * time.Second)
return nil
}
func (s *targetSystemd) connect() (incus.InstanceServer, error) {
if util.PathExists("/run/incus/unix.socket") {
return incus.ConnectIncusUnix("/run/incus/unix.socket", &incus.ConnectionArgs{SkipGetServer: true})
}
return incus.ConnectIncusUnix("/var/lib/incus/unix.socket", &incus.ConnectionArgs{SkipGetServer: true})
}
func (s *targetSystemd) paths() (*daemonPaths, error) {
return &daemonPaths{
daemon: "/var/lib/incus",
logs: "/var/log/incus",
cache: "/var/cache/incus",
}, nil
}
func (s *targetSystemd) name() string {
return "systemd"
}

View File

@ -1,60 +0,0 @@
package main
import (
"time"
incus "github.com/lxc/incus/v7/client"
"github.com/lxc/incus/v7/shared/subprocess"
"github.com/lxc/incus/v7/shared/util"
)
type targetXbps struct{}
func (s *targetXbps) present() bool {
if !util.PathExists("/var/db/xbps/.incus-files.plist") {
return false
}
if !util.PathExists("/var/service/incus") {
return false
}
if !util.PathExists("/var/lib/incus/unix.socket") {
return false
}
return true
}
func (s *targetXbps) stop() error {
_, err := subprocess.RunCommand("sv", "stop", "incus")
return err
}
func (s *targetXbps) start() error {
_, err := subprocess.RunCommand("sv", "start", "incus")
if err != nil {
return err
}
// Wait for the socket to become available.
time.Sleep(5 * time.Second)
return nil
}
func (s *targetXbps) connect() (incus.InstanceServer, error) {
return incus.ConnectIncusUnix("/var/lib/incus/unix.socket", &incus.ConnectionArgs{SkipGetServer: true})
}
func (s *targetXbps) paths() (*daemonPaths, error) {
return &daemonPaths{
daemon: "/var/lib/incus",
logs: "/var/log/incus",
cache: "/var/cache/incus",
}, nil
}
func (s *targetXbps) name() string {
return "xbps"
}

View File

@ -1,435 +0,0 @@
package main
import (
"errors"
"fmt"
"os"
"os/exec"
"github.com/lxc/incus/v7/internal/linux"
"github.com/lxc/incus/v7/internal/version"
"github.com/lxc/incus/v7/shared/api"
"github.com/lxc/incus/v7/shared/util"
)
var (
minLXDVersion = &version.DottedVersion{Major: 4, Minor: 0, Patch: 0}
maxLXDVersion = &version.DottedVersion{Major: 5, Minor: 21, Patch: 99}
)
func (c *cmdMigrate) validate(source source, target target) error {
srcClient, err := source.connect()
if err != nil {
return fmt.Errorf("Failed to connect to source: %v", err)
}
targetClient, err := target.connect()
if err != nil {
return fmt.Errorf("Failed to connect to target: %v", err)
}
// Get versions.
fmt.Println("=> Checking server versions")
srcServerInfo, _, err := srcClient.GetServer()
if err != nil {
return fmt.Errorf("Failed getting source server info: %w", err)
}
targetServerInfo, _, err := targetClient.GetServer()
if err != nil {
return fmt.Errorf("Failed getting target server info: %w", err)
}
fmt.Printf("==> Source version: %s\n", srcServerInfo.Environment.ServerVersion)
fmt.Printf("==> Target version: %s\n", targetServerInfo.Environment.ServerVersion)
// Compare versions.
fmt.Println("=> Validating version compatibility")
srcVersion, err := version.Parse(srcServerInfo.Environment.ServerVersion)
if err != nil {
return fmt.Errorf("Couldn't parse source server version: %w", err)
}
if srcVersion.Compare(minLXDVersion) < 0 {
return fmt.Errorf("LXD version is lower than minimal version %q", minLXDVersion)
}
if !c.flagIgnoreVersionCheck {
if srcVersion.Compare(maxLXDVersion) > 0 {
return fmt.Errorf("LXD version is newer than maximum version %q", maxLXDVersion)
}
} else {
fmt.Println("==> WARNING: User asked to bypass version check")
}
// Validate source non-empty.
srcCheckEmpty := func() (bool, error) {
// Check if more than one project.
names, err := srcClient.GetProjectNames()
if err != nil {
return false, err
}
if len(names) > 1 {
return false, nil
}
// Check if more than one profile.
names, err = srcClient.GetProfileNames()
if err != nil {
return false, err
}
if len(names) > 1 {
return false, nil
}
// Check if any instance is present.
names, err = srcClient.GetInstanceNames(api.InstanceTypeAny)
if err != nil {
return false, err
}
if len(names) > 0 {
return false, nil
}
// Check if any storage pool is present.
names, err = srcClient.GetStoragePoolNames()
if err != nil {
return false, err
}
if len(names) > 0 {
return false, nil
}
// Check if any network is present.
networks, err := srcClient.GetNetworks()
if err != nil {
return false, err
}
for _, network := range networks {
if network.Managed {
return false, nil
}
}
return true, nil
}
fmt.Println("=> Checking that the source server isn't empty")
isEmpty, err := srcCheckEmpty()
if err != nil {
return fmt.Errorf("Failed to check source server: %w", err)
}
if isEmpty {
return errors.New("Source server is empty, migration not needed")
}
// Validate target empty.
targetCheckEmpty := func() (bool, string, error) {
// Check if more than one project.
names, err := targetClient.GetProjectNames()
if err != nil {
return false, "", err
}
if len(names) > 1 {
return false, "projects", nil
}
// Check if more than one profile.
names, err = targetClient.GetProfileNames()
if err != nil {
return false, "", err
}
if len(names) > 1 {
return false, "profiles", nil
}
// Check if any instance is present.
names, err = targetClient.GetInstanceNames(api.InstanceTypeAny)
if err != nil {
return false, "", err
}
if len(names) > 0 {
return false, "instances", nil
}
// Check if any storage pool is present.
names, err = targetClient.GetStoragePoolNames()
if err != nil {
return false, "", err
}
if len(names) > 0 {
return false, "storage pools", nil
}
// Check if any network is present.
networks, err := targetClient.GetNetworks()
if err != nil {
return false, "", err
}
for _, network := range networks {
if network.Managed {
return false, "networks", nil
}
}
return true, "", nil
}
fmt.Println("=> Checking that the target server is empty")
isEmpty, found, err := targetCheckEmpty()
if err != nil {
return fmt.Errorf("Failed to check target server: %w", err)
}
if !isEmpty {
return fmt.Errorf("Target server isn't empty (%s found), can't proceed with migration.", found)
}
// Validate configuration.
validationErrors := []error{}
fmt.Println("=> Validating source server configuration")
deprecatedConfigs := []string{
"candid.api.key",
"candid.api.url",
"candid.domains",
"candid.expiry",
"core.trust_password",
"maas.api.key",
"maas.api.url",
"rbac.agent.url",
"rbac.agent.username",
"rbac.agent.private_key",
"rbac.agent.public_key",
"rbac.api.expiry",
"rbac.api.key",
"rbac.api.url",
"rbac.expiry",
}
for _, key := range deprecatedConfigs {
_, ok := srcServerInfo.Config[key]
if ok {
validationErrors = append(validationErrors, fmt.Errorf("Source server is using deprecated server configuration key %q", key))
}
}
networks, err := srcClient.GetNetworks()
if err != nil {
return fmt.Errorf("Couldn't list source networks: %w", err)
}
deprecatedNetworkConfigs := []string{
"bridge.mode",
"fan.overlay_subnet",
"fan.underlay_subnet",
"fan.type",
}
for _, network := range networks {
if !network.Managed {
continue
}
for _, key := range deprecatedNetworkConfigs {
_, ok := network.Config[key]
if ok {
validationErrors = append(validationErrors, fmt.Errorf("Source server has network %q using deprecated configuration key %q", network.Name, key))
}
}
}
storagePools, err := srcClient.GetStoragePools()
if err != nil {
return fmt.Errorf("Couldn't list storage pools: %w", err)
}
for _, pool := range storagePools {
switch pool.Driver {
case "zfs":
_, err = exec.LookPath("zfs")
if err != nil {
validationErrors = append(validationErrors, fmt.Errorf("Required command %q is missing for storage pool %q", "zfs", pool.Name))
}
case "btrfs":
_, err = exec.LookPath("btrfs")
if err != nil {
validationErrors = append(validationErrors, fmt.Errorf("Required command %q is missing for storage pool %q", "btrfs", pool.Name))
}
case "ceph", "cephfs", "cephobject":
_, err = exec.LookPath("ceph")
if err != nil {
validationErrors = append(validationErrors, fmt.Errorf("Required command %q is missing for storage pool %q", "ceph", pool.Name))
}
case "lvm", "lvmcluster":
_, err = exec.LookPath("lvm")
if err != nil {
validationErrors = append(validationErrors, fmt.Errorf("Required command %q is missing for storage pool %q", "lvm", pool.Name))
}
if pool.Driver == "lvmcluster" {
_, err = exec.LookPath("lvmlockctl")
if err != nil {
validationErrors = append(validationErrors, fmt.Errorf("Required command %q is missing for storage pool %q", "lvmlockctl", pool.Name))
}
}
}
}
deprecatedInstanceConfigs := []string{
"boot.debug_edk2",
"limits.network.priority",
"security.devlxd",
"security.devlxd.images",
}
deprecatedInstanceDeviceConfigs := []string{
"maas.subnet.ipv4",
"maas.subnet.ipv6",
}
projects, err := srcClient.GetProjects()
if err != nil {
return fmt.Errorf("Couldn't list source projects: %w", err)
}
for _, project := range projects {
c := srcClient.UseProject(project.Name)
instances, err := c.GetInstances(api.InstanceTypeAny)
if err != nil {
return fmt.Errorf("Couldn't list instances in project %q: %w", project.Name, err)
}
for _, inst := range instances {
for _, key := range deprecatedInstanceConfigs {
_, ok := inst.Config[key]
if ok {
validationErrors = append(validationErrors, fmt.Errorf("Source server has instance %q in project %q using deprecated configuration key %q", inst.Name, project.Name, key))
}
}
for deviceName, device := range inst.Devices {
for _, key := range deprecatedInstanceDeviceConfigs {
_, ok := device[key]
if ok {
validationErrors = append(validationErrors, fmt.Errorf("Source server has device %q for instance %q in project %q using deprecated configuration key %q", deviceName, inst.Name, project.Name, key))
}
}
}
}
profiles, err := c.GetProfiles()
if err != nil {
return fmt.Errorf("Couldn't list profiles in project %q: %w", project.Name, err)
}
for _, profile := range profiles {
for _, key := range deprecatedInstanceConfigs {
_, ok := profile.Config[key]
if ok {
validationErrors = append(validationErrors, fmt.Errorf("Source server has profile %q in project %q using deprecated configuration key %q", profile.Name, project.Name, key))
}
}
for deviceName, device := range profile.Devices {
for _, key := range deprecatedInstanceDeviceConfigs {
_, ok := device[key]
if ok {
validationErrors = append(validationErrors, fmt.Errorf("Source server has device %q for profile %q in project %q using deprecated configuration key %q", deviceName, profile.Name, project.Name, key))
}
}
}
}
}
// Cluster validation.
if srcServerInfo.Environment.ServerClustered {
clusterMembers, err := srcClient.GetClusterMembers()
if err != nil {
return errors.New("Failed to retrieve the list of cluster members")
}
for _, member := range clusterMembers {
if member.Status != "Online" {
if os.Getenv("CLUSTER_NO_STOP") == "1" && member.Status == "Evacuated" {
continue
}
validationErrors = append(validationErrors, fmt.Errorf("Cluster member %q isn't in the online state", member.ServerName))
}
}
}
if len(validationErrors) > 0 {
fmt.Println("")
fmt.Println("Source server uses obsolete features:")
for _, err := range validationErrors {
fmt.Printf(" - %s\n", err.Error())
}
return errors.New("Source server is using incompatible configuration")
}
// Storage validation.
targetPaths, err := target.paths()
if err != nil {
return fmt.Errorf("Failed to get target paths: %w", err)
}
sourcePaths, err := source.paths()
if err != nil {
return fmt.Errorf("Failed to get source paths: %w", err)
}
fi, err := os.Lstat(sourcePaths.daemon)
if err != nil {
return err
}
if fi.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("The source path %q is a symlink. Incus does not support its daemon directory being a symlink, please switch to a bind-mount.", sourcePaths.daemon)
}
if linux.IsMountPoint(targetPaths.daemon) {
return fmt.Errorf("The target path %q is a mountpoint. This isn't currently supported as the target path needs to be deleted during the migration.", targetPaths.daemon)
}
srcFilesystem, _ := linux.DetectFilesystem(sourcePaths.daemon)
targetFilesystem, _ := linux.DetectFilesystem(targetPaths.daemon)
if srcFilesystem == "btrfs" && targetFilesystem != "btrfs" && !linux.IsMountPoint(sourcePaths.daemon) {
return errors.New("Source daemon running on btrfs but being moved to non-btrfs target")
}
// Shiftfs check.
if util.PathExists("/sys/module/shiftfs/") {
fmt.Println("")
fmt.Println("WARNING: The shiftfs kernel module was detected on your system.")
fmt.Println(" This may indicate that your LXD installation is using shiftfs")
fmt.Println(" to allow shifted passthrough of some disks to your instance.")
fmt.Println("")
fmt.Println(" Incus does not support shiftfs but instead relies on a recent")
fmt.Println(" feature of the Linux kernel instead, VFS idmap.")
fmt.Println("")
fmt.Println(" If your instances actively rely on shiftfs today, you may need")
fmt.Println(" to update to a more recent Linux kernel or ZFS version to keep")
fmt.Println(" using this shifted passthrough features.")
fmt.Println("")
}
return nil
}

View File

@ -1,74 +0,0 @@
(server-migrate-lxd)=
# Migrating from LXD
Incus includes a tool named `lxd-to-incus` which can be used to convert an existing LXD installation into an Incus one.
For this to work properly, you should make sure to {doc}`install </installing>` the latest stable release of Incus but not initialize it.
Instead, make sure that both `incus info` and `lxc info` both work properly, then run `lxd-to-incus` to migrate your data.
This process transfers the entire database and all storage from LXD to Incus, resulting in an identical setup after the migration.
```{note}
Following the migration, you will need to add any user that was in the `lxd` group into the equivalent `incus-admin` group.
As group membership only updates on login, users may need to close their session and re-open it for it to take effect.
```
```{note}
Additionally, this process doesn't migrate the command line tool configuration.
For this you may want to transfer the content of `~/.config/lxc/` or `~/snap/lxd/common/config/` over to `~/.config/incus/`.
This is mostly useful to those who interact with other remote servers or have configured custom aliases.
```
```{terminal}
:input: lxd-to-incus
:user: root
=> Looking for source server
==> Detected: snap package
=> Looking for target server
=> Connecting to source server
=> Connecting to the target server
=> Checking server versions
==> Source version: 5.19
==> Target version: 0.1
=> Validating version compatibility
=> Checking that the source server isn't empty
=> Checking that the target server is empty
=> Validating source server configuration
The migration is now ready to proceed.
At this point, the source server and all its instances will be stopped.
Instances will come back online once the migration is complete.
Proceed with the migration? [default=no]: yes
=> Stopping the source server
=> Stopping the target server
=> Wiping the target server
=> Migrating the data
=> Migrating database
=> Cleaning up target paths
=> Starting the target server
=> Checking the target server
Uninstall the LXD package? [default=no]: yes
=> Uninstalling the source server
```
```{terminal}
:input: incus list
:user: root
To start your first container, try: incus launch images:debian/12
Or for a virtual machine: incus launch images:debian/12 --vm
+------+---------+-----------------------+------------------------------------------------+-----------+-----------+
| NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS |
+------+---------+-----------------------+------------------------------------------------+-----------+-----------+
| u1 | RUNNING | 10.204.220.101 (eth0) | fd42:1eb6:f1d8:4e2a:1266:6aff:fe65:940d (eth0) | CONTAINER | 0 |
+------+---------+-----------------------+------------------------------------------------+-----------+-----------+
```
The tool will also look for any configuration that is incompatible with Incus and fail before any data is migrated.
```{warning}
All instances will be stopped during the migration.
Once the migration process is started, it cannot easily be reversed so make sure to plan adequate downtime.
```

View File

@ -101,7 +101,6 @@ There are two options currently available to Debian users.
On Debian systems, running `apt install incus` will get Incus installed with all dependencies required for running containers and virtual machines.
If you only wish to run containers in Incus, you can run just `apt install incus-base`.
If migrating from LXD, also run `apt install incus-extra` to get the `lxd-to-incus` command.
1. Zabbly package repository
@ -189,8 +188,6 @@ Install Incus with:
zypper in incus
If migrating from LXD, please also install `incus-tools` for `lxd-to-incus`.
The default setup should work fine for most users, but if you intend to run many containers on your system you may wish to apply some custom `sysctl` settings [as suggested in the production deployments guide](./reference/server_settings.md).
Please report packaging issues [here](https://bugzilla.opensuse.org/).
@ -233,7 +230,6 @@ There are two options currently available to Ubuntu users.
A native `incus` package is currently available in Ubuntu 24.04 LTS and later.
On such systems, just running `apt install incus` will get Incus installed.
To run virtual machines, also run `apt install qemu-system`.
If migrating from LXD, also run `apt install incus-tools` to get the `lxd-to-incus` command.
1. Zabbly package repository

View File

@ -42,7 +42,6 @@ On top of those, the following optional binaries may also be made available:
- `incus-benchmark`
- `incus-migrate`
- `lxc-to-incus`
- `lxd-to-incus` (should be kept to root only)
## Incus agent binaries

View File

@ -4,7 +4,6 @@
```{toctree}
:maxdepth: 1
Migrating from LXD </howto/server_migrate_lxd>
Configure the server <howto/server_configure>
/server_config
System settings <reference/server_settings>

View File

@ -30,10 +30,6 @@ After going through these steps, you will have a general idea of how to use Incu
1. Initialize Incus
```{note}
If you are migrating from an existing LXD installation, skip this step and refer to {ref}`server-migrate-lxd` instead.
```
Incus requires some initial setup for networking and storage. This can be done interactively through:
incus admin init