mirror of
https://github.com/lxc/incus
synced 2026-08-02 05:26:46 +00:00
Remove lxd-to-incus
Signed-off-by: Stéphane Graber <stgraber@stgraber.org>
This commit is contained in:
parent
4596075585
commit
fe296b7400
8
.github/workflows/tests.yml
vendored
8
.github/workflows/tests.yml
vendored
@ -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
1
.gitignore
vendored
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
package main
|
||||
|
||||
type daemonPaths struct {
|
||||
daemon string
|
||||
logs string
|
||||
cache string
|
||||
}
|
||||
@ -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{},
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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{},
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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.
|
||||
```
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user