From fe296b7400c7c3400a1a43e49ac9f7e76077e44c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graber?= Date: Thu, 4 Jun 2026 14:23:01 -0400 Subject: [PATCH] Remove lxd-to-incus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stéphane Graber --- .github/workflows/tests.yml | 8 - .gitignore | 1 - .goreleaser.yaml | 24 - README.md | 2 - cmd/lxd-to-incus/db.go | 135 ---- cmd/lxd-to-incus/main.go | 951 ---------------------------- cmd/lxd-to-incus/ovn.go | 283 --------- cmd/lxd-to-incus/paths.go | 7 - cmd/lxd-to-incus/sources.go | 22 - cmd/lxd-to-incus/sources_apk.go | 49 -- cmd/lxd-to-incus/sources_copr.go | 54 -- cmd/lxd-to-incus/sources_deb.go | 53 -- cmd/lxd-to-incus/sources_manual.go | 66 -- cmd/lxd-to-incus/sources_snap.go | 53 -- cmd/lxd-to-incus/sources_xbps.go | 56 -- cmd/lxd-to-incus/targets.go | 18 - cmd/lxd-to-incus/targets_openrc.go | 73 --- cmd/lxd-to-incus/targets_systemd.go | 57 -- cmd/lxd-to-incus/targets_xbps.go | 60 -- cmd/lxd-to-incus/validate.go | 435 ------------- doc/howto/server_migrate_lxd.md | 74 --- doc/installing.md | 4 - doc/packaging.md | 1 - doc/server.md | 1 - doc/tutorial/first_steps.md | 4 - 25 files changed, 2491 deletions(-) delete mode 100644 cmd/lxd-to-incus/db.go delete mode 100644 cmd/lxd-to-incus/main.go delete mode 100644 cmd/lxd-to-incus/ovn.go delete mode 100644 cmd/lxd-to-incus/paths.go delete mode 100644 cmd/lxd-to-incus/sources.go delete mode 100644 cmd/lxd-to-incus/sources_apk.go delete mode 100644 cmd/lxd-to-incus/sources_copr.go delete mode 100644 cmd/lxd-to-incus/sources_deb.go delete mode 100644 cmd/lxd-to-incus/sources_manual.go delete mode 100644 cmd/lxd-to-incus/sources_snap.go delete mode 100644 cmd/lxd-to-incus/sources_xbps.go delete mode 100644 cmd/lxd-to-incus/targets.go delete mode 100644 cmd/lxd-to-incus/targets_openrc.go delete mode 100644 cmd/lxd-to-incus/targets_systemd.go delete mode 100644 cmd/lxd-to-incus/targets_xbps.go delete mode 100644 cmd/lxd-to-incus/validate.go delete mode 100644 doc/howto/server_migrate_lxd.md diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5f4147ff4e..3262904c46 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 diff --git a/.gitignore b/.gitignore index faf2784e0a..c49fbf1525 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 4ed3048958..5114bb02d9 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -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 diff --git a/README.md b/README.md index 9e738d51b1..5d7f1530ef 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/cmd/lxd-to-incus/db.go b/cmd/lxd-to-incus/db.go deleted file mode 100644 index 0abd313f33..0000000000 --- a/cmd/lxd-to-incus/db.go +++ /dev/null @@ -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", ×tamp, &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 -} diff --git a/cmd/lxd-to-incus/main.go b/cmd/lxd-to-incus/main.go deleted file mode 100644 index 350e79afd9..0000000000 --- a/cmd/lxd-to-incus/main.go +++ /dev/null @@ -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 -} diff --git a/cmd/lxd-to-incus/ovn.go b/cmd/lxd-to-incus/ovn.go deleted file mode 100644 index 9a3073be87..0000000000 --- a/cmd/lxd-to-incus/ovn.go +++ /dev/null @@ -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 -} diff --git a/cmd/lxd-to-incus/paths.go b/cmd/lxd-to-incus/paths.go deleted file mode 100644 index 23ac25057e..0000000000 --- a/cmd/lxd-to-incus/paths.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -type daemonPaths struct { - daemon string - logs string - cache string -} diff --git a/cmd/lxd-to-incus/sources.go b/cmd/lxd-to-incus/sources.go deleted file mode 100644 index 11e9cf2f0d..0000000000 --- a/cmd/lxd-to-incus/sources.go +++ /dev/null @@ -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{}, -} diff --git a/cmd/lxd-to-incus/sources_apk.go b/cmd/lxd-to-incus/sources_apk.go deleted file mode 100644 index 6b298e91be..0000000000 --- a/cmd/lxd-to-incus/sources_apk.go +++ /dev/null @@ -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 -} diff --git a/cmd/lxd-to-incus/sources_copr.go b/cmd/lxd-to-incus/sources_copr.go deleted file mode 100644 index 68fb0f8988..0000000000 --- a/cmd/lxd-to-incus/sources_copr.go +++ /dev/null @@ -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 -} diff --git a/cmd/lxd-to-incus/sources_deb.go b/cmd/lxd-to-incus/sources_deb.go deleted file mode 100644 index 24a0c5b336..0000000000 --- a/cmd/lxd-to-incus/sources_deb.go +++ /dev/null @@ -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 -} diff --git a/cmd/lxd-to-incus/sources_manual.go b/cmd/lxd-to-incus/sources_manual.go deleted file mode 100644 index eb2d0bd6fa..0000000000 --- a/cmd/lxd-to-incus/sources_manual.go +++ /dev/null @@ -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 -} diff --git a/cmd/lxd-to-incus/sources_snap.go b/cmd/lxd-to-incus/sources_snap.go deleted file mode 100644 index 32a4e223fb..0000000000 --- a/cmd/lxd-to-incus/sources_snap.go +++ /dev/null @@ -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 -} diff --git a/cmd/lxd-to-incus/sources_xbps.go b/cmd/lxd-to-incus/sources_xbps.go deleted file mode 100644 index 9952f27fb7..0000000000 --- a/cmd/lxd-to-incus/sources_xbps.go +++ /dev/null @@ -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 -} diff --git a/cmd/lxd-to-incus/targets.go b/cmd/lxd-to-incus/targets.go deleted file mode 100644 index 4f7ea95fa2..0000000000 --- a/cmd/lxd-to-incus/targets.go +++ /dev/null @@ -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{}, -} diff --git a/cmd/lxd-to-incus/targets_openrc.go b/cmd/lxd-to-incus/targets_openrc.go deleted file mode 100644 index 4c8bcd6e1e..0000000000 --- a/cmd/lxd-to-incus/targets_openrc.go +++ /dev/null @@ -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" -} diff --git a/cmd/lxd-to-incus/targets_systemd.go b/cmd/lxd-to-incus/targets_systemd.go deleted file mode 100644 index 0ae0b56795..0000000000 --- a/cmd/lxd-to-incus/targets_systemd.go +++ /dev/null @@ -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" -} diff --git a/cmd/lxd-to-incus/targets_xbps.go b/cmd/lxd-to-incus/targets_xbps.go deleted file mode 100644 index 5c8611a5ce..0000000000 --- a/cmd/lxd-to-incus/targets_xbps.go +++ /dev/null @@ -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" -} diff --git a/cmd/lxd-to-incus/validate.go b/cmd/lxd-to-incus/validate.go deleted file mode 100644 index dd52dbb80a..0000000000 --- a/cmd/lxd-to-incus/validate.go +++ /dev/null @@ -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 -} diff --git a/doc/howto/server_migrate_lxd.md b/doc/howto/server_migrate_lxd.md deleted file mode 100644 index 2125f36761..0000000000 --- a/doc/howto/server_migrate_lxd.md +++ /dev/null @@ -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 ` 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. -``` diff --git a/doc/installing.md b/doc/installing.md index 9ca96a0a70..e014c4d4e1 100644 --- a/doc/installing.md +++ b/doc/installing.md @@ -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 diff --git a/doc/packaging.md b/doc/packaging.md index 703356da2d..5f60fb205e 100644 --- a/doc/packaging.md +++ b/doc/packaging.md @@ -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 diff --git a/doc/server.md b/doc/server.md index 8369b77c2a..7ae2c52d46 100644 --- a/doc/server.md +++ b/doc/server.md @@ -4,7 +4,6 @@ ```{toctree} :maxdepth: 1 -Migrating from LXD Configure the server /server_config System settings diff --git a/doc/tutorial/first_steps.md b/doc/tutorial/first_steps.md index 25e24db36f..28573d2025 100644 --- a/doc/tutorial/first_steps.md +++ b/doc/tutorial/first_steps.md @@ -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