[lxc-devel] [lxd/master] lxc-to-lxd: Rewrite in Go

monstermunchkin on Github lxc-bot at linuxcontainers.org
Wed Jul 4 11:27:55 UTC 2018


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 301 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20180704/b931ab78/attachment.bin>
-------------- next part --------------
From cb3f1097cdefda8842d734d4efd4e6042393fb4f Mon Sep 17 00:00:00 2001
From: Thomas Hipp <thomas.hipp at canonical.com>
Date: Wed, 27 Jun 2018 20:52:57 +0200
Subject: [PATCH 1/3] lxc-to-lxd: Rewrite in Go

Signed-off-by: Thomas Hipp <thomas.hipp at canonical.com>
---
 lxc-to-lxd/config.go       | 214 ++++++++++++++++
 lxc-to-lxd/main.go         |  44 ++++
 lxc-to-lxd/main_migrate.go | 602 +++++++++++++++++++++++++++++++++++++++++++++
 lxc-to-lxd/main_netcat.go  |  72 ++++++
 lxc-to-lxd/network.go      |  31 +++
 lxc-to-lxd/transfer.go     | 141 +++++++++++
 lxc-to-lxd/utils.go        | 182 ++++++++++++++
 7 files changed, 1286 insertions(+)
 create mode 100644 lxc-to-lxd/config.go
 create mode 100644 lxc-to-lxd/main.go
 create mode 100644 lxc-to-lxd/main_migrate.go
 create mode 100644 lxc-to-lxd/main_netcat.go
 create mode 100644 lxc-to-lxd/network.go
 create mode 100644 lxc-to-lxd/transfer.go
 create mode 100644 lxc-to-lxd/utils.go

diff --git a/lxc-to-lxd/config.go b/lxc-to-lxd/config.go
new file mode 100644
index 000000000..615e7eb11
--- /dev/null
+++ b/lxc-to-lxd/config.go
@@ -0,0 +1,214 @@
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/lxc/lxd/shared"
+)
+
+var checkedKeys = []string{
+	"lxc.aa_allow_incomplete",
+	"lxc.aa_profile",
+	"lxc.apparmor.allow_incomplete",
+	"lxc.apparmor.profile",
+	"lxc.arch",
+	"lxc.autodev",
+	"lxc.cap.drop",
+	"lxc.environment",
+	"lxc.haltsignal",
+	"lxc.id_map",
+	"lxc.idmap",
+	"lxc.include",
+	"lxc.loglevel",
+	"lxc.mount",
+	"lxc.mount.auto",
+	"lxc.mount.entry",
+	"lxc.pts",
+	"lxc.pty.max",
+	"lxc.rebootsignal",
+	"lxc.rootfs",
+	"lxc.rootfs.backend",
+	"lxc.rootfs.mount",
+	"lxc.rootfs.path",
+	"lxc.seccomp",
+	"lxc.signal.halt",
+	"lxc.signal.reboot",
+	"lxc.signal.stop",
+	"lxc.start.auto",
+	"lxc.start.delay",
+	"lxc.start.order",
+	"lxc.stopsignal",
+	"lxc.tty",
+	"lxc.tty.max",
+	"lxc.uts.name",
+	"lxc.utsname",
+	"lxd.migrated",
+}
+
+func getUnsupportedKeys(config []string) []string {
+	var out []string
+
+	for _, a := range config {
+		supported := false
+
+		for _, b := range checkedKeys {
+			if a == b {
+				supported = true
+				break
+			}
+		}
+
+		if !supported {
+			out = append(out, a)
+		}
+	}
+
+	return out
+}
+
+func getConfig(config []string, key string) []string {
+	// Return an array since keys can be specified more than once
+	var out []string
+
+	for _, c := range config {
+		text := strings.TrimSpace(c)
+
+		// Ignore empty lines and comments
+		if len(text) == 0 || strings.HasPrefix(text, "#") {
+			continue
+		}
+
+		line := strings.Split(text, "=")
+		k := strings.TrimSpace(line[0])
+		v := strings.Trim(strings.TrimSpace(line[1]), "\"")
+
+		if k == key && len(v) > 0 {
+			out = append(out, v)
+		}
+	}
+
+	if len(out) == 0 {
+		return nil
+	}
+
+	return out
+}
+
+func getConfigKeys(config []string) []string {
+	// Make sure we don't have duplicate keys
+	m := make(map[string]bool, 0)
+	for _, c := range config {
+		text := strings.TrimSpace(c)
+
+		// Ignore empty lines and comments
+		if len(text) == 0 || strings.HasPrefix(text, "#") {
+			continue
+		}
+
+		line := strings.Split(text, "=")
+		key := strings.TrimSpace(line[0])
+		if strings.HasPrefix(key, "lxc.") {
+			m[key] = true
+		}
+	}
+
+	var out []string
+	for k := range m {
+		out = append(out, k)
+	}
+
+	return out
+}
+
+func parseConfig(path string) ([]string, error) {
+	file, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer file.Close()
+
+	var config []string
+
+	// Parse config
+	sc := bufio.NewScanner(file)
+	for sc.Scan() {
+		text := strings.TrimSpace(sc.Text())
+
+		// Ignore empty lines and comments
+		if len(text) == 0 || strings.HasPrefix(text, "#") {
+			continue
+		}
+
+		line := strings.Split(text, "=")
+		key := strings.TrimSpace(line[0])
+		value := strings.TrimSpace(line[1])
+
+		switch key {
+		// Parse user-added includes
+		case "lxc.include":
+			// Ignore our own default configs
+			if strings.HasPrefix(value, "/usr/share/lxc/config/") {
+				continue
+			}
+
+			if shared.PathExists(value) {
+				if shared.IsDir(value) {
+					files, err := ioutil.ReadDir(value)
+					if err != nil {
+						return nil, err
+					}
+
+					for _, file := range files {
+						path := filepath.Join(value, file.Name())
+						if !strings.HasSuffix(path, ".conf") {
+							continue
+						}
+
+						config = append(config, path)
+					}
+				} else {
+					c, err := parseConfig(value)
+					if err != nil {
+						return nil, err
+					}
+
+					config = append(config, c...)
+				}
+				continue
+			}
+		// Expand any fstab
+		case "lxc.mount":
+			if !shared.PathExists(value) {
+				fmt.Println("Container fstab file doesn't exist, skipping...")
+				continue
+			}
+
+			file, err := os.Open(value)
+			if err != nil {
+				return nil, err
+			}
+			defer file.Close()
+
+			sc := bufio.NewScanner(file)
+			for sc.Scan() {
+				text := strings.TrimSpace(sc.Text())
+
+				if len(text) > 0 && !strings.HasPrefix(text, "#") {
+					config = append(config, fmt.Sprintf("lxc.mount.entry = %s", text))
+				}
+			}
+
+			continue
+
+		default:
+			config = append(config, text)
+		}
+	}
+
+	return config, nil
+}
diff --git a/lxc-to-lxd/main.go b/lxc-to-lxd/main.go
new file mode 100644
index 000000000..a350214ed
--- /dev/null
+++ b/lxc-to-lxd/main.go
@@ -0,0 +1,44 @@
+package main
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+
+	"github.com/lxc/lxd/shared/version"
+)
+
+type cmdGlobal struct {
+	flagVersion bool
+	flagHelp    bool
+}
+
+func main() {
+	// migrate command (main)
+	migrateCmd := cmdMigrate{}
+	app := migrateCmd.Command()
+	app.SilenceUsage = true
+
+	// Workaround for main command
+	app.Args = cobra.ArbitraryArgs
+
+	// Global flags
+	globalCmd := cmdGlobal{}
+	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
+
+	// netcat sub-command
+	netcatCmd := cmdNetcat{global: &globalCmd}
+	app.AddCommand(netcatCmd.Command())
+
+	// Run the main command and handle errors
+	err := app.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
diff --git a/lxc-to-lxd/main_migrate.go b/lxc-to-lxd/main_migrate.go
new file mode 100644
index 000000000..f973e3b21
--- /dev/null
+++ b/lxc-to-lxd/main_migrate.go
@@ -0,0 +1,602 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+	"runtime"
+	"strconv"
+	"strings"
+
+	"github.com/spf13/cobra"
+	lxc "gopkg.in/lxc/go-lxc.v2"
+
+	lxd "github.com/lxc/lxd/client"
+	"github.com/lxc/lxd/lxc/config"
+	"github.com/lxc/lxd/lxc/utils"
+	"github.com/lxc/lxd/lxd/types"
+	"github.com/lxc/lxd/shared"
+	"github.com/lxc/lxd/shared/api"
+	"github.com/lxc/lxd/shared/i18n"
+	"github.com/lxc/lxd/shared/osarch"
+)
+
+type cmdMigrate struct {
+	global *cmdGlobal
+
+	conf     *config.Config
+	confPath string
+	cmd      *cobra.Command
+
+	// Flags
+	flagDryRun     bool
+	flagDebug      bool
+	flagAll        bool
+	flagDelete     bool
+	flagStorage    string
+	flagLXCPath    string
+	flagRsyncArgs  string
+	flagContainers []string
+}
+
+func (c *cmdMigrate) Command() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "lxc-to-lxd",
+		Short: i18n.G("Command line client for container migration"),
+	}
+
+	// Wrappers
+	cmd.RunE = c.RunE
+
+	// Flags
+	cmd.Flags().BoolVar(&c.flagDryRun, "dry-run", false, i18n.G("Dry run mode"))
+	cmd.Flags().BoolVar(&c.flagDebug, "debug", false, i18n.G("Print debugging output"))
+	cmd.Flags().BoolVar(&c.flagAll, "all", false, i18n.G("Import all containers"))
+	cmd.Flags().BoolVar(&c.flagDelete, "delete", false, i18n.G("Delete the source container"))
+	cmd.Flags().StringVar(&c.flagStorage, "storage", "",
+		i18n.G("Storage pool to use for the container")+"``")
+	cmd.Flags().StringVar(&c.flagLXCPath, "lxcpath", lxc.DefaultConfigPath(),
+		i18n.G("Alternate LXC path")+"``")
+	cmd.Flags().StringVar(&c.flagRsyncArgs, "rsync-args", "",
+		"Extra arguments to pass to rsync"+"``")
+	cmd.Flags().StringSliceVar(&c.flagContainers, "containers", nil,
+		i18n.G("Container(s) to import")+"``")
+
+	return cmd
+}
+
+func (c *cmdMigrate) RunE(cmd *cobra.Command, args []string) error {
+	if (len(c.flagContainers) == 0 && !c.flagAll) || (len(c.flagContainers) > 0 && c.flagAll) {
+		fmt.Fprintln(os.Stderr, "You must either pass container names or --all")
+		os.Exit(1)
+	}
+	// Connect to LXD
+	d, err := lxd.ConnectLXDUnix("", nil)
+	if err != nil {
+		return err
+	}
+
+	// Retrieve LXC containers
+	for _, container := range lxc.Containers(c.flagLXCPath) {
+		if !c.flagAll && !shared.StringInSlice(container.Name(), c.flagContainers) {
+			continue
+		}
+
+		err := convertContainer(d, container, c.flagStorage,
+			c.flagDryRun, c.flagRsyncArgs, c.flagDebug)
+		if err != nil {
+			fmt.Printf("Skipping container '%s': %v\n", container.Name(), err)
+			continue
+		}
+
+		// Delete container
+		if c.flagDelete {
+			if c.flagDryRun {
+				fmt.Println("Would destroy container now")
+			} else {
+				err := container.Destroy()
+				if err != nil {
+					fmt.Printf("Failed to destroy container '%s': %v\n", container.Name(), err)
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+func validateConfig(conf []string, container *lxc.Container) error {
+	// Checking whether container has already been migrated
+	fmt.Println("Checking whether container has already been migrated")
+	if len(getConfig(conf, "lxd.migrated")) > 0 {
+		return fmt.Errorf("Container has already been migrated")
+	}
+
+	// Validate lxc.utsname / lxc.uts.name
+	value := getConfig(conf, "lxc.uts.name")
+	if value == nil {
+		value = getConfig(conf, "lxc.utsname")
+	}
+	if value == nil || value[0] != container.Name() {
+		return fmt.Errorf("Container name doesn't match lxc.uts.name / lxc.utsname")
+	}
+
+	// Validate lxc.aa_allow_incomplete: must be set to 0 or unset.
+	fmt.Println("Validating whether incomplete AppArmor support is enabled")
+	value = getConfig(conf, "lxc.apparmor.allow_incomplete")
+	if value == nil {
+		value = getConfig(conf, "lxc.aa_allow_incomplete")
+	}
+	if value != nil {
+		v, err := strconv.Atoi(value[0])
+		if err != nil {
+			return err
+		}
+
+		if v != 0 {
+			return fmt.Errorf("Container allows incomplete AppArmor support")
+		}
+	}
+
+	// Validate lxc.autodev: must be set to 1 or unset.
+	fmt.Println("Validating whether mounting a minimal /dev is enabled")
+	value = getConfig(conf, "lxc.autodev")
+	if value != nil {
+		v, err := strconv.Atoi(value[0])
+		if err != nil {
+			return err
+		}
+
+		if v != 1 {
+			return fmt.Errorf("Container doesn't mount a minimal /dev filesystem")
+		}
+	}
+
+	// Extract and valid rootfs key
+	fmt.Println("Validating container rootfs")
+	rootfs, err := getRootfs(conf)
+	if err != nil {
+		return err
+	}
+
+	if !shared.PathExists(rootfs) {
+		return fmt.Errorf("Couldn't find the container rootfs '%s'", rootfs)
+	}
+
+	return nil
+}
+
+func convertContainer(d lxd.ContainerServer, container *lxc.Container, storage string,
+	dryRun bool, rsyncArgs string, debug bool) error {
+	// Don't migrate running containers
+	if container.Running() {
+		return fmt.Errorf("Only stopped containers can be migrated")
+	}
+
+	fmt.Println("Parsing LXC configuration")
+	conf, err := parseConfig(container.ConfigFileName())
+	if err != nil {
+		return err
+	}
+
+	if debug {
+		fmt.Printf("Container configuration:\n  %v\n", strings.Join(conf, "\n  "))
+	}
+
+	// Check whether there are unsupported keys in the config
+	fmt.Println("Checking for unsupported LXC configuration keys")
+	keys := getUnsupportedKeys(getConfigKeys(conf))
+	for _, k := range keys {
+		if !strings.HasPrefix(k, "lxc.net.") &&
+			!strings.HasPrefix(k, "lxc.network.") &&
+			!strings.HasPrefix(k, "lxc.cgroup.") &&
+			!strings.HasPrefix(k, "lxc.cgroup2.") {
+			return fmt.Errorf("Found unsupported config key '%s'", k)
+		}
+	}
+
+	// Make sure we don't have a conflict
+	fmt.Println("Checking for existing containers")
+	containers, err := d.GetContainerNames()
+	if err != nil {
+		return err
+	}
+
+	found := false
+	for _, name := range containers {
+		if name == container.Name() {
+			found = true
+		}
+	}
+	if found {
+		return fmt.Errorf("Container already exists")
+	}
+
+	// Validate config
+	err = validateConfig(conf, container)
+	if err != nil {
+		return err
+	}
+
+	newConfig := make(map[string]string, 0)
+
+	value := getConfig(conf, "lxd.idmap")
+	if value == nil {
+		value = getConfig(conf, "lxd.id_map")
+	}
+	if value == nil {
+		// Privileged container
+		newConfig["security.privileged"] = "true"
+	} else {
+		// Unprivileged container
+		newConfig["security.privileged"] = "false"
+	}
+
+	newDevices := make(types.Devices, 0)
+
+	// Convert network configuration
+	err = convertNetworkConfig(container, newDevices)
+	if err != nil {
+		return err
+	}
+
+	// Convert storage configuration
+	err = convertStorageConfig(conf, newDevices)
+	if err != nil {
+		return err
+	}
+
+	// Convert environment
+	fmt.Println("Processing environment configuration")
+	value = getConfig(conf, "lxc.environment")
+	for _, env := range value {
+		entry := strings.Split(env, "=")
+		key := strings.TrimSpace(entry[0])
+		val := strings.TrimSpace(entry[len(entry)-1])
+		newConfig[fmt.Sprintf("environment.%s", key)] = val
+	}
+
+	// Convert auto-start
+	fmt.Println("Processing container boot configuration")
+	value = getConfig(conf, "lxc.start.auto")
+	if value != nil {
+		val, err := strconv.Atoi(value[0])
+		if err != nil {
+			return err
+		}
+
+		if val > 0 {
+			newConfig["boot.autostart"] = "true"
+		}
+	}
+
+	value = getConfig(conf, "lxc.start.delay")
+	if value != nil {
+		val, err := strconv.Atoi(value[0])
+		if err != nil {
+			return err
+		}
+
+		if val > 0 {
+			newConfig["boot.autostart.delay"] = value[0]
+		}
+	}
+
+	value = getConfig(conf, "lxc.start.order")
+	if value != nil {
+		val, err := strconv.Atoi(value[0])
+		if err != nil {
+			return err
+		}
+
+		if val > 0 {
+			newConfig["boot.autostart.priority"] = value[0]
+		}
+	}
+
+	// Convert apparmor
+	fmt.Println("Processing container apparmor configuration")
+	value = getConfig(conf, "lxc.apparmor.profile")
+	if value == nil {
+		value = getConfig(conf, "lxc.aa_profile")
+	}
+	if value != nil {
+		if value[0] == "lxc-container-default-with-nesting" {
+			newConfig["security.nesting"] = "true"
+		} else if value[0] != "lxc-container-default" {
+			newConfig["raw.lxc"] = fmt.Sprintf("lxc.aa_profile=%s\n", value[0])
+		}
+	}
+
+	// Convert seccomp
+	fmt.Println("Processing container seccomp configuration")
+	value = getConfig(conf, "lxc.seccomp.profile")
+	if value == nil {
+		value = getConfig(conf, "lxc.seccomp")
+	}
+	if value != nil && value[0] != "/usr/share/lxc/config/common.seccomp" {
+		return fmt.Errorf("Custom seccomp profiles aren't supported")
+	}
+
+	// Convert SELinux
+	fmt.Println("Processing container SELinux configuration")
+	value = getConfig(conf, "lxc.selinux.context")
+	if value == nil {
+		value = getConfig(conf, "lxc.se_context")
+	}
+	if value != nil {
+		return fmt.Errorf("Custom SELinux policies aren't supported")
+	}
+
+	// Convert capabilities
+	fmt.Println("Processing container capabilities configuration")
+	value = getConfig(conf, "lxc.cap.drop")
+	if value != nil {
+		for _, cap := range strings.Split(value[0], " ") {
+			// Ignore capabilities that are dropped in LXD containers by default.
+			if shared.StringInSlice(cap, []string{"mac_admin", "mac_override", "sys_module",
+				"sys_time"}) {
+				continue
+			}
+			return fmt.Errorf("Custom capabilities aren't supported")
+		}
+	}
+
+	value = getConfig(conf, "lxc.cap.keep")
+	if value != nil {
+		return fmt.Errorf("Custom capabilities aren't supported")
+	}
+
+	// Add rest of the keys to lxc.raw
+	for _, c := range conf {
+		parts := strings.SplitN(c, "=", 2)
+		if len(parts) != 2 {
+			continue
+		}
+
+		key := strings.TrimSpace(parts[0])
+		val := strings.TrimSpace(parts[1])
+
+		switch key {
+		case "lxc.signal.halt", "lxc.haltsignal":
+			newConfig["raw.lxc"] += fmt.Sprintf("lxc.signal.halt=%s\n", val)
+		case "lxc.signal.reboot", "lxc.rebootsignal":
+			newConfig["raw.lxc"] += fmt.Sprintf("lxc.signal.reboot=%s\n", val)
+		case "lxc.signal.stop", "lxc.stopsignal":
+			newConfig["raw.lxc"] += fmt.Sprintf("lxc.signal.stop=%s\n", val)
+		case "lxc.apparmor.allow_incomplete", "lxc.aa_allow_incomplete":
+			newConfig["raw.lxc"] += fmt.Sprintf("lxc.apparmor.allow_incomplete=%s\n", val)
+		case "lxc.pty.max", "lxc.pts":
+			newConfig["raw.lxc"] += fmt.Sprintf("lxc.pty.max=%s\n", val)
+		case "lxc.tty.max", "lxc.tty":
+			newConfig["raw.lxc"] += fmt.Sprintf("lxc.tty.max=%s\n", val)
+		}
+	}
+
+	// Setup the container creation request
+	req := api.ContainersPost{
+		Name: container.Name(),
+		Source: api.ContainerSource{
+			Type: "migration",
+			Mode: "push",
+		},
+	}
+	req.Config = newConfig
+	req.Devices = newDevices
+	req.Profiles = []string{"default"}
+
+	// Set the container architecture if set in LXC
+	fmt.Println("Processing container architecture configuration")
+	var arch string
+	value = getConfig(conf, "lxc.arch")
+	if value == nil {
+		fmt.Println("Couldn't find container architecture, assuming native")
+		arch = runtime.GOARCH
+	} else {
+		arch = value[0]
+	}
+
+	archId, err := osarch.ArchitectureId(arch)
+	if err != nil {
+		return err
+	}
+
+	req.Architecture, err = osarch.ArchitectureName(archId)
+	if err != nil {
+		return err
+	}
+
+	if storage != "" {
+		req.Devices["root"] = map[string]string{
+			"type": "disk",
+			"pool": storage,
+			"path": "/",
+		}
+	}
+
+	if debug {
+		out, _ := json.MarshalIndent(req, "", "  ")
+		fmt.Printf("LXD container config:\n%v\n", string(out))
+	}
+
+	// Create container
+	fmt.Println("Creating container")
+	if dryRun {
+		fmt.Println("Would create container now")
+	} else {
+		op, err := d.CreateContainer(req)
+		if err != nil {
+			return err
+		}
+
+		progress := utils.ProgressRenderer{Format: "Transferring container: %s"}
+		_, err = op.AddHandler(progress.UpdateOp)
+		if err != nil {
+			progress.Done("")
+			return err
+		}
+
+		rootfs, _ := getRootfs(conf)
+
+		err = transferRootfs(d, op, rootfs, rsyncArgs)
+		if err != nil {
+			return err
+		}
+
+		progress.Done(fmt.Sprintf("Container '%s' successfully created", container.Name()))
+	}
+
+	return nil
+}
+
+func convertNetworkConfig(container *lxc.Container, devices types.Devices) error {
+	networkDevice := func(network map[string]string) map[string]string {
+		if network == nil {
+			return nil
+		}
+
+		device := make(map[string]string, 0)
+		device["type"] = "nic"
+
+		// Get the device type
+		device["nictype"] = network["type"]
+
+		// Convert the configuration
+		for k, v := range network {
+			switch k {
+			case "hwaddr", "mtu", "name":
+				device[k] = v
+			case "link":
+				device["parent"] = v
+			case "veth_pair":
+				device["host_name"] = v
+			case "":
+				// empty key
+				return nil
+			}
+		}
+
+		switch device["nictype"] {
+		case "veth":
+			_, ok := device["parent"]
+			if ok {
+				device["nictype"] = "bridged"
+			} else {
+				device["nictype"] = "p2p"
+			}
+		case "phys":
+			device["nictype"] = "physical"
+		case "empty":
+			return nil
+		}
+
+		return device
+	}
+
+	fmt.Println("Processing network configuration")
+
+	devices["eth0"] = make(map[string]string, 0)
+	devices["eth0"]["type"] = "none"
+
+	// New config key
+	for i, _ := range container.ConfigItem("lxc.net") {
+		network := networkGet(container, i, "lxc.net")
+
+		dev := networkDevice(network)
+		if dev == nil {
+			continue
+		}
+
+		devices[fmt.Sprintf("convert_net%d", i)] = dev
+	}
+
+	// Old config key
+	for i, _ := range container.ConfigItem("lxc.network") {
+		network := networkGet(container, i, "lxc.network")
+
+		dev := networkDevice(network)
+		if dev == nil {
+			continue
+		}
+
+		devices[fmt.Sprintf("convert_net%d", len(devices))] = dev
+	}
+
+	return nil
+}
+
+func convertStorageConfig(conf []string, devices types.Devices) error {
+	fmt.Println("Processing storage configuration")
+
+	i := 0
+	for _, mount := range getConfig(conf, "lxc.mount.entry") {
+		parts := strings.Split(mount, " ")
+		if len(parts) < 4 {
+			return fmt.Errorf("Invalid mount configuration: %s", mount)
+		}
+
+		// Ignore mounts that are present in LXD containers by default.
+		if shared.StringInSlice(parts[0], []string{"proc", "sysfs"}) {
+			continue
+		}
+
+		device := make(map[string]string, 0)
+		device["type"] = "disk"
+
+		// Deal with read-only mounts
+		if shared.StringInSlice("ro", strings.Split(parts[3], ",")) {
+			device["readonly"] = "true"
+		}
+
+		// Deal with optional mounts
+		if shared.StringInSlice("optional", strings.Split(parts[3], ",")) {
+			device["optional"] = "true"
+		} else {
+			if strings.HasPrefix(parts[0], "/") {
+				if !shared.PathExists(parts[0]) {
+					return fmt.Errorf("Invalid path: %s", parts[0])
+				}
+			} else {
+				continue
+			}
+		}
+
+		// Set the source
+		device["source"] = parts[0]
+
+		// Figure out the target
+		if !strings.HasPrefix(parts[1], "/") {
+			device["path"] = fmt.Sprintf("/%s", parts[1])
+		} else {
+			rootfs, err := getRootfs(conf)
+			if err != nil {
+				return err
+			}
+			device["path"] = strings.TrimPrefix(parts[1], rootfs)
+		}
+
+		devices[fmt.Sprintf("convert_mount%d", i)] = device
+		i++
+	}
+
+	return nil
+}
+
+func getRootfs(conf []string) (string, error) {
+	value := getConfig(conf, "lxc.rootfs.path")
+	if value == nil {
+		value = getConfig(conf, "lxc.rootfs")
+		if value == nil {
+			return "", fmt.Errorf("Invalid container, missing lxc.rootfs key")
+		}
+	}
+
+	// XXX: ignore the first part (storage) for now
+	parts := strings.Split(value[0], ":")
+
+	if len(parts) != 2 {
+		return "", fmt.Errorf("Invalid container, invalid lxc.rootfs key")
+	}
+
+	return parts[1], nil
+}
diff --git a/lxc-to-lxd/main_netcat.go b/lxc-to-lxd/main_netcat.go
new file mode 100644
index 000000000..db21bed71
--- /dev/null
+++ b/lxc-to-lxd/main_netcat.go
@@ -0,0 +1,72 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"net"
+	"os"
+	"sync"
+
+	"github.com/spf13/cobra"
+
+	"github.com/lxc/lxd/shared/eagain"
+)
+
+type cmdNetcat struct {
+	global *cmdGlobal
+}
+
+func (c *cmdNetcat) Command() *cobra.Command {
+	cmd := &cobra.Command{}
+
+	cmd.Use = "netcat <address>"
+	cmd.Short = "Sends stdin data to a unix socket"
+	cmd.RunE = c.Run
+	cmd.Hidden = true
+
+	return cmd
+}
+
+func (c *cmdNetcat) Run(cmd *cobra.Command, args []string) error {
+	// Help and usage
+	if len(args) == 0 {
+		cmd.Help()
+		return nil
+	}
+
+	// Handle mandatory arguments
+	if len(args) != 1 {
+		cmd.Help()
+		return fmt.Errorf("Missing required argument")
+	}
+
+	// Connect to the provided address
+	uAddr, err := net.ResolveUnixAddr("unix", args[0])
+	if err != nil {
+		return err
+	}
+
+	conn, err := net.DialUnix("unix", nil, uAddr)
+	if err != nil {
+		return err
+	}
+
+	// We'll wait until we're done reading from the socket
+	wg := sync.WaitGroup{}
+	wg.Add(1)
+
+	go func() {
+		io.Copy(eagain.Writer{Writer: os.Stdout}, eagain.Reader{Reader: conn})
+		conn.Close()
+		wg.Done()
+	}()
+
+	go func() {
+		io.Copy(eagain.Writer{Writer: conn}, eagain.Reader{Reader: os.Stdin})
+	}()
+
+	// Wait
+	wg.Wait()
+
+	return nil
+}
diff --git a/lxc-to-lxd/network.go b/lxc-to-lxd/network.go
new file mode 100644
index 000000000..97570064e
--- /dev/null
+++ b/lxc-to-lxd/network.go
@@ -0,0 +1,31 @@
+package main
+
+import (
+	"fmt"
+	"strings"
+
+	lxc "gopkg.in/lxc/go-lxc.v2"
+)
+
+func networkGet(container *lxc.Container, index int, configKey string) map[string]string {
+	keys := container.ConfigKeys(fmt.Sprintf("%s.%d", configKey, index))
+	if len(keys) == 0 {
+		return nil
+	}
+
+	dev := make(map[string]string, 0)
+	for _, k := range keys {
+		value := container.ConfigItem(fmt.Sprintf("%s.%d.%s", configKey, index, k))
+		if len(value) == 0 || strings.TrimSpace(value[0]) == "" {
+			continue
+		}
+
+		dev[k] = value[0]
+	}
+
+	if len(dev) == 0 {
+		return nil
+	}
+
+	return dev
+}
diff --git a/lxc-to-lxd/transfer.go b/lxc-to-lxd/transfer.go
new file mode 100644
index 000000000..c190d4d05
--- /dev/null
+++ b/lxc-to-lxd/transfer.go
@@ -0,0 +1,141 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net"
+	"os"
+	"os/exec"
+	"strings"
+
+	"github.com/gorilla/websocket"
+	"github.com/pborman/uuid"
+
+	"github.com/lxc/lxd/lxd/migration"
+	"github.com/lxc/lxd/shared"
+	"github.com/lxc/lxd/shared/version"
+)
+
+// Send an rsync stream of a path over a websocket
+func rsyncSend(conn *websocket.Conn, path string, rsyncArgs string) error {
+	cmd, dataSocket, stderr, err := rsyncSendSetup(path, rsyncArgs)
+	if err != nil {
+		return err
+	}
+
+	if dataSocket != nil {
+		defer dataSocket.Close()
+	}
+
+	readDone, writeDone := shared.WebsocketMirror(conn, dataSocket, io.ReadCloser(dataSocket), nil, nil)
+
+	output, err := ioutil.ReadAll(stderr)
+	if err != nil {
+		cmd.Process.Kill()
+		cmd.Wait()
+		return fmt.Errorf("Failed to rsync: %v\n%s", err, output)
+	}
+
+	err = cmd.Wait()
+	<-readDone
+	<-writeDone
+
+	if err != nil {
+		return fmt.Errorf("Failed to rsync: %v\n%s", err, output)
+	}
+
+	return nil
+}
+
+// Spawn the rsync process
+func rsyncSendSetup(path string, rsyncArgs string) (*exec.Cmd, net.Conn, io.ReadCloser, error) {
+	auds := fmt.Sprintf("@lxc-to-lxd/%s", uuid.NewRandom().String())
+	if len(auds) > shared.ABSTRACT_UNIX_SOCK_LEN-1 {
+		auds = auds[:shared.ABSTRACT_UNIX_SOCK_LEN-1]
+	}
+
+	l, err := net.Listen("unix", auds)
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	execPath, err := os.Readlink("/proc/self/exe")
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	rsyncCmd := fmt.Sprintf("sh -c \"%s netcat %s\"", execPath, auds)
+
+	args := []string{
+		"-ar",
+		"--devices",
+		"--numeric-ids",
+		"--partial",
+		"--sparse",
+	}
+
+	// Ignore deletions (requires 3.1 or higher)
+	rsyncCheckVersion := func(min string) bool {
+		out, err := shared.RunCommand("rsync", "--version")
+		if err != nil {
+			return false
+		}
+
+		fields := strings.Split(out, " ")
+		curVer, err := version.Parse(fields[3])
+		if err != nil {
+			return false
+		}
+
+		minVer, err := version.Parse(min)
+		if err != nil {
+			return false
+		}
+
+		return curVer.Compare(minVer) >= 0
+	}
+
+	if rsyncCheckVersion("3.1.0") {
+		args = append(args, "--ignore-missing-args")
+	}
+
+	if rsyncArgs != "" {
+		args = append(args, strings.Split(rsyncArgs, " ")...)
+	}
+
+	args = append(args, []string{path, "localhost:/tmp/foo"}...)
+	args = append(args, []string{"-e", rsyncCmd}...)
+
+	cmd := exec.Command("rsync", args...)
+	cmd.Stdout = os.Stderr
+
+	stderr, err := cmd.StderrPipe()
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	if err := cmd.Start(); err != nil {
+		return nil, nil, nil, err
+	}
+
+	conn, err := l.Accept()
+	if err != nil {
+		cmd.Process.Kill()
+		cmd.Wait()
+		return nil, nil, nil, err
+	}
+	l.Close()
+
+	return cmd, conn, stderr, nil
+}
+
+func protoSendError(ws *websocket.Conn, err error) {
+	migration.ProtoSendControl(ws, err)
+
+	if err != nil {
+		closeMsg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")
+		ws.WriteMessage(websocket.CloseMessage, closeMsg)
+		ws.Close()
+	}
+}
diff --git a/lxc-to-lxd/utils.go b/lxc-to-lxd/utils.go
new file mode 100644
index 000000000..bcad65d8b
--- /dev/null
+++ b/lxc-to-lxd/utils.go
@@ -0,0 +1,182 @@
+package main
+
+import (
+	"crypto/x509"
+	"encoding/pem"
+	"fmt"
+	"strings"
+	"syscall"
+
+	"golang.org/x/crypto/ssh/terminal"
+
+	"github.com/lxc/lxd/client"
+	"github.com/lxc/lxd/lxd/migration"
+	"github.com/lxc/lxd/shared"
+	"github.com/lxc/lxd/shared/api"
+)
+
+func transferRootfs(dst lxd.ContainerServer, op lxd.Operation, rootfs string, rsyncArgs string) error {
+	opAPI := op.Get()
+
+	// Connect to the websockets
+	wsControl, err := op.GetWebsocket(opAPI.Metadata["control"].(string))
+	if err != nil {
+		return err
+	}
+
+	wsFs, err := op.GetWebsocket(opAPI.Metadata["fs"].(string))
+	if err != nil {
+		return err
+	}
+
+	// Setup control struct
+	fs := migration.MigrationFSType_RSYNC
+	header := migration.MigrationHeader{
+		Fs: &fs,
+	}
+
+	err = migration.ProtoSend(wsControl, &header)
+	if err != nil {
+		protoSendError(wsControl, err)
+		return err
+	}
+
+	err = migration.ProtoRecv(wsControl, &header)
+	if err != nil {
+		protoSendError(wsControl, err)
+		return err
+	}
+
+	// Send the filesystem
+	abort := func(err error) error {
+		protoSendError(wsControl, err)
+		return err
+	}
+
+	err = rsyncSend(wsFs, rootfs, rsyncArgs)
+	if err != nil {
+		return abort(err)
+	}
+
+	// Check the result
+	msg := migration.MigrationControl{}
+	err = migration.ProtoRecv(wsControl, &msg)
+	if err != nil {
+		wsControl.Close()
+		return err
+	}
+
+	if !*msg.Success {
+		return fmt.Errorf(*msg.Message)
+	}
+
+	return nil
+}
+
+func connectTarget(url string) (lxd.ContainerServer, error) {
+	// Generate a new client certificate for this
+	fmt.Println("Generating a temporary client certificate. This may take a minute...")
+	clientCrt, clientKey, err := shared.GenerateMemCert(true)
+	if err != nil {
+		return nil, err
+	}
+
+	// Attempt to connect using the system CA
+	args := lxd.ConnectionArgs{}
+	args.TLSClientCert = string(clientCrt)
+	args.TLSClientKey = string(clientKey)
+	args.UserAgent = "LXC-TO-LXD"
+	c, err := lxd.ConnectLXD(url, &args)
+
+	var certificate *x509.Certificate
+	if err != nil {
+		// Failed to connect using the system CA, so retrieve the remote certificate
+		certificate, err = shared.GetRemoteCertificate(url)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// Handle certificate prompt
+	if certificate != nil {
+		digest := shared.CertFingerprint(certificate)
+
+		fmt.Printf("Certificate fingerprint: %s\n", digest)
+		fmt.Printf("ok (y/n)? ")
+		line, err := shared.ReadStdin()
+		if err != nil {
+			return nil, err
+		}
+
+		if len(line) < 1 || line[0] != 'y' && line[0] != 'Y' {
+			return nil, fmt.Errorf("Server certificate rejected by user")
+		}
+
+		serverCrt := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certificate.Raw})
+		args.TLSServerCert = string(serverCrt)
+
+		// Setup a new connection, this time with the remote certificate
+		c, err = lxd.ConnectLXD(url, &args)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// Get server information
+	srv, _, err := c.GetServer()
+	if err != nil {
+		return nil, err
+	}
+
+	// Check if our cert is already trusted
+	if srv.Auth == "trusted" {
+		return c, nil
+	}
+
+	// Prompt for trust password
+	fmt.Printf("Admin password for %s: ", url)
+	pwd, err := terminal.ReadPassword(0)
+	if err != nil {
+		return nil, err
+	}
+	fmt.Println("")
+
+	// Add client certificate to trust store
+	req := api.CertificatesPost{
+		Password: string(pwd),
+	}
+	req.Type = "client"
+
+	err = c.CreateCertificate(req)
+	if err != nil {
+		return nil, err
+	}
+
+	return c, nil
+}
+
+func setupSource(path string, mounts []string) error {
+	prefix := "/"
+	if len(mounts) > 0 {
+		prefix = mounts[0]
+	}
+
+	// Mount everything
+	for _, mount := range mounts {
+		target := fmt.Sprintf("%s/%s", path, strings.TrimPrefix(mount, prefix))
+
+		// Mount the path
+		err := syscall.Mount(mount, target, "none", syscall.MS_BIND, "")
+		if err != nil {
+			return fmt.Errorf("Failed to mount %s: %v", mount, err)
+		}
+
+		// Make it read-only
+		err = syscall.Mount("", target, "none", syscall.MS_BIND|syscall.MS_RDONLY|syscall.MS_REMOUNT, "")
+		if err != nil {
+			return fmt.Errorf("Failed to make %s read-only: %v", mount, err)
+		}
+	}
+
+	return nil
+}

From 9db8e09fd107c1b5e5433fb0a09522fc22bff7d3 Mon Sep 17 00:00:00 2001
From: Thomas Hipp <thomas.hipp at canonical.com>
Date: Wed, 27 Jun 2018 20:54:00 +0200
Subject: [PATCH 2/3] tests: Add lxc-to-lxd tests

Signed-off-by: Thomas Hipp <thomas.hipp at canonical.com>
---
 lxc-to-lxd/main_migrate_test.go | 374 ++++++++++++++++++++++++++++++++++++++++
 test/suites/lxc-to-lxd.sh       |  60 +++++++
 2 files changed, 434 insertions(+)
 create mode 100644 lxc-to-lxd/main_migrate_test.go
 create mode 100644 test/suites/lxc-to-lxd.sh

diff --git a/lxc-to-lxd/main_migrate_test.go b/lxc-to-lxd/main_migrate_test.go
new file mode 100644
index 000000000..a0f5fbb02
--- /dev/null
+++ b/lxc-to-lxd/main_migrate_test.go
@@ -0,0 +1,374 @@
+package main
+
+import (
+	"io/ioutil"
+	"log"
+	"os"
+	"strings"
+	"testing"
+
+	"github.com/lxc/lxd/lxd/types"
+	"github.com/stretchr/testify/require"
+	lxc "gopkg.in/lxc/go-lxc.v2"
+)
+
+func TestValidateConfig(t *testing.T) {
+	tests := []struct {
+		name       string
+		config     []string
+		err        string
+		shouldFail bool
+	}{
+		{
+			"container migrated",
+			[]string{
+				"lxd.migrated = 1",
+			},
+			"Container has already been migrated",
+			true,
+		},
+		{
+			"container name missmatch (1)",
+			[]string{
+				"lxc.uts.name = c2",
+			},
+			"Container name doesn't match lxc.uts.name / lxc.utsname",
+			true,
+		},
+		{
+			"container name missmatch (2)",
+			[]string{
+				"lxc.utsname = c2",
+			},
+			"Container name doesn't match lxc.uts.name / lxc.utsname",
+			true,
+		},
+		{
+			"incomplete AppArmor support (1)",
+			[]string{
+				"lxc.uts.name = c1",
+				"lxc.apparmor.allow_incomplete = 1",
+			},
+			"Container allows incomplete AppArmor support",
+			true,
+		},
+		{
+			"incomplete AppArmor support (2)",
+			[]string{
+				"lxc.uts.name = c1",
+				"lxc.aa_allow_incomplete = 1",
+			},
+			"Container allows incomplete AppArmor support",
+			true,
+		},
+		{
+			"missing minimal /dev filesystem",
+			[]string{
+				"lxc.uts.name = c1",
+				"lxc.apparmor.allow_incomplete = 0",
+				"lxc.autodev = 0",
+			},
+			"Container doesn't mount a minimal /dev filesystem",
+			true,
+		},
+		{
+			"missing lxc.rootfs key",
+			[]string{
+				"lxc.uts.name = c1",
+				"lxc.apparmor.allow_incomplete = 0",
+				"lxc.autodev = 1",
+			},
+			"Invalid container, missing lxc.rootfs key",
+			true,
+		},
+		{
+			"invalid lxc.rootfs key",
+			[]string{
+				"lxc.uts.name = c1",
+				"lxc.apparmor.allow_incomplete = 0",
+				"lxc.autodev = 1",
+				"lxc.rootfs = /invalid/path",
+			},
+			"Invalid container, invalid lxc.rootfs key",
+			true,
+		},
+		{
+			"non-existent rootfs path",
+			[]string{
+				"lxc.uts.name = c1",
+				"lxc.apparmor.allow_incomplete = 0",
+				"lxc.autodev = 1",
+				"lxc.rootfs = dir:/invalid/path",
+			},
+			"Couldn't find the container rootfs '/invalid/path'",
+			true,
+		},
+	}
+
+	lxcPath, err := ioutil.TempDir("", "lxc-to-lxd-test-")
+	require.NoError(t, err)
+	defer os.RemoveAll(lxcPath)
+
+	c, err := lxc.NewContainer("c1", lxcPath)
+	require.NoError(t, err)
+
+	for i, tt := range tests {
+		log.Printf("Running test #%d: %s", i, tt.name)
+		err := validateConfig(tt.config, c)
+		if tt.shouldFail {
+			require.EqualError(t, err, tt.err)
+		} else {
+			require.NoError(t, err)
+		}
+	}
+}
+
+func TestConvertNetworkConfig(t *testing.T) {
+	tests := []struct {
+		name            string
+		config          []string
+		expectedDevices types.Devices
+		expectedError   string
+		shouldFail      bool
+	}{
+		{
+			"loopback only",
+			[]string{},
+			types.Devices{
+				"eth0": map[string]string{
+					"type": "none",
+				},
+			},
+			"",
+			false,
+		},
+		{
+			"multiple network devices",
+			[]string{
+				"lxc.net.1.type = macvlan",
+				"lxc.net.1.macvlan.mode = bridge",
+				"lxc.net.1.link = mvlan0",
+				"lxc.net.1.hwaddr = 00:16:3e:8d:4f:51",
+				"lxc.net.1.name = eth1",
+				"lxc.net.2.type = veth",
+				"lxc.net.2.link = lxcbr0",
+				"lxc.net.2.hwaddr = 00:16:3e:a2:7d:54",
+				"lxc.net.2.name = eth2",
+			},
+			types.Devices{
+				"convert_net2": map[string]string{
+					"type":    "nic",
+					"nictype": "bridged",
+					"parent":  "lxcbr0",
+					"name":    "eth2",
+					"hwaddr":  "00:16:3e:a2:7d:54",
+				},
+				"eth0": map[string]string{
+					"type": "none",
+				},
+				"convert_net1": map[string]string{
+					"name":    "eth1",
+					"hwaddr":  "00:16:3e:8d:4f:51",
+					"type":    "nic",
+					"nictype": "macvlan",
+					"parent":  "mvlan0",
+				},
+			},
+			"",
+			false,
+		},
+	}
+
+	lxcPath, err := ioutil.TempDir("", "lxc-to-lxd-test-")
+	require.NoError(t, err)
+	defer os.RemoveAll(lxcPath)
+
+	for i, tt := range tests {
+		log.Printf("Running test #%d: %s", i, tt.name)
+
+		c, err := lxc.NewContainer("c1", lxcPath)
+		require.NoError(t, err)
+
+		err = c.Create(lxc.TemplateOptions{Template: "busybox"})
+		require.NoError(t, err)
+
+		for _, conf := range tt.config {
+			parts := strings.SplitN(conf, "=", 2)
+			require.Equal(t, 2, len(parts))
+			err := c.SetConfigItem(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]))
+			require.NoError(t, err)
+		}
+
+		devices := make(types.Devices, 0)
+		err = convertNetworkConfig(c, devices)
+		if tt.shouldFail {
+			require.EqualError(t, err, tt.expectedError)
+		} else {
+			require.NoError(t, err)
+			require.Equal(t, tt.expectedDevices, devices)
+		}
+
+		err = c.Destroy()
+		require.NoError(t, err)
+	}
+}
+
+func TestConvertStorageConfig(t *testing.T) {
+	tests := []struct {
+		name            string
+		config          []string
+		expectedDevices types.Devices
+		expectedError   string
+		shouldFail      bool
+	}{
+		{
+			"invalid path",
+			[]string{
+				"lxc.mount.entry = /foo lib none ro,bind 0 0",
+			},
+			types.Devices{},
+			"Invalid path: /foo",
+			true,
+		},
+		{
+			"invalid rootfs",
+			[]string{
+				"lxc.rootfs.path = /invalid",
+				"lxc.mount.entry = /lib /lib none ro,bind 0 0",
+			},
+			types.Devices{},
+			"Invalid container, invalid lxc.rootfs key",
+			true,
+		},
+		{
+			"ignored default mounts",
+			[]string{
+				"lxc.mount.entry = proc /proc proc defaults 0 0",
+			},
+			types.Devices{},
+			"",
+			false,
+		},
+		{
+			"ignored mounts",
+			[]string{
+				"lxc.mount.entry = shm /dev/shm tmpfs defaults 0 0",
+			},
+			types.Devices{},
+			"",
+			false,
+		},
+		{
+			"valid mount configuration",
+			[]string{
+				"lxc.rootfs.path = dir:/tmp",
+				"lxc.mount.entry = /lib lib none ro,bind 0 0",
+				"lxc.mount.entry = /usr/lib usr/lib none ro,bind 1 0",
+				"lxc.mount.entry = /lib64 lib64 none ro,bind 0 0",
+				"lxc.mount.entry = /usr/lib64 usr/lib64 none ro,bind 1 0",
+				"lxc.mount.entry = /sys/kernel/security /sys/kernel/security none ro,bind,optional 1 0",
+				"lxc.mount.entry = /mnt /tmp/mnt none ro,bind 0 0",
+			},
+			types.Devices{
+				"convert_mount0": map[string]string{
+					"type":     "disk",
+					"readonly": "true",
+					"source":   "/lib",
+					"path":     "/lib",
+				},
+				"convert_mount1": map[string]string{
+					"type":     "disk",
+					"readonly": "true",
+					"source":   "/usr/lib",
+					"path":     "/usr/lib",
+				},
+				"convert_mount2": map[string]string{
+					"type":     "disk",
+					"readonly": "true",
+					"source":   "/lib64",
+					"path":     "/lib64",
+				},
+				"convert_mount3": map[string]string{
+					"type":     "disk",
+					"readonly": "true",
+					"source":   "/usr/lib64",
+					"path":     "/usr/lib64",
+				},
+				"convert_mount4": map[string]string{
+					"type":     "disk",
+					"readonly": "true",
+					"optional": "true",
+					"source":   "/sys/kernel/security",
+					"path":     "/sys/kernel/security",
+				},
+				"convert_mount5": map[string]string{
+					"type":     "disk",
+					"readonly": "true",
+					"source":   "/mnt",
+					"path":     "/mnt",
+				},
+			},
+			"",
+			false,
+		},
+	}
+
+	for i, tt := range tests {
+		log.Printf("Running test #%d: %s", i, tt.name)
+		devices := make(types.Devices, 0)
+		err := convertStorageConfig(tt.config, devices)
+		if tt.shouldFail {
+			require.EqualError(t, err, tt.expectedError)
+		} else {
+			require.NoError(t, err)
+			require.Equal(t, tt.expectedDevices, devices)
+		}
+	}
+}
+
+func TestGetRootfs(t *testing.T) {
+	tests := []struct {
+		name           string
+		config         []string
+		expectedOutput string
+		expectedError  string
+		shouldFail     bool
+	}{
+		{
+			"missing lxc.rootfs key",
+			[]string{},
+			"",
+			"Invalid container, missing lxc.rootfs key",
+			true,
+		},
+		{
+			"invalid lxc.rootfs key",
+			[]string{
+				"lxc.rootfs = foobar",
+			},
+			"",
+			"Invalid container, invalid lxc.rootfs key",
+			true,
+		},
+		{
+			"valid lxc.rootfs key",
+			[]string{
+				"lxc.rootfs = dir:foobar",
+			},
+			"foobar",
+			"",
+			false,
+		},
+	}
+
+	for i, tt := range tests {
+		log.Printf("Running test #%d: %s", i, tt.name)
+		rootfs, err := getRootfs(tt.config)
+		require.Equal(t, tt.expectedOutput, rootfs)
+		if tt.shouldFail {
+			require.EqualError(t, err, tt.expectedError)
+		} else {
+			require.NoError(t, err)
+		}
+	}
+}
diff --git a/test/suites/lxc-to-lxd.sh b/test/suites/lxc-to-lxd.sh
new file mode 100644
index 000000000..238c7e7ef
--- /dev/null
+++ b/test/suites/lxc-to-lxd.sh
@@ -0,0 +1,60 @@
+test_lxc_to_lxd() {
+  ensure_has_localhost_remote "${LXD_ADDR}"
+
+  LXC_DIR="${TEST_DIR}/lxc"
+
+  mkdir -p "${LXC_DIR}"
+
+  # Create LXC containers
+  lxc-create -P "${LXC_DIR}" -n c1 -B dir -t busybox
+  lxc-create -P "${LXC_DIR}" -n c2 -B dir -t busybox
+  lxc-create -P "${LXC_DIR}" -n c3 -B dir -t busybox
+
+  # Convert single LXC container (dry run)
+  lxc-to-lxd --lxcpath "${LXC_DIR}" --dry-run --delete --containers c1
+
+  # Ensure the LXC containers have not been deleted
+  [[ $(lxc-ls -P "${LXC_DIR}" -1 | wc -l) -eq 3 ]]
+
+  # Ensure no containers have been converted
+  ! lxc info c1
+  ! lxc info c2
+  ! lxc info c3
+
+  # Convert single LXC container
+  lxc-to-lxd --lxcpath "${LXC_DIR}" --containers c1
+
+  # Ensure the LXC containers have not been deleted
+  [[ $(lxc-ls -P "${LXC_DIR}" -1 | wc -l) -eq 3 ]]
+
+  # Ensure only c1 has been converted
+  lxc info c1
+  ! lxc info c2
+  ! lxc info c3
+
+  # Ensure the converted container is startable
+  lxc start c1
+  lxc delete -f c1
+
+  # Convert some LXC containers
+  lxc-to-lxd --lxcpath "${LXC_DIR}" --delete --containers c1,c2
+
+  # Ensure the LXC containers c1 and c2 have been deleted
+  [[ $(lxc-ls -P "${LXC_DIR}" -1 | wc -l) -eq 1 ]]
+
+  # Ensure all containers have been converted
+  lxc info c1
+  lxc info c2
+  ! lxc info c3
+
+  # Convert all LXC containers
+  lxc-to-lxd --lxcpath "${LXC_DIR}" --delete --all
+
+  # Ensure the remaining LXC containers have been deleted
+  [[ $(lxc-ls -P "${LXC_DIR}" -1 | wc -l) -eq 0 ]]
+
+  # Ensure all containers have been converted
+  lxc info c1
+  lxc info c2
+  lxc info c3
+}

From 85e45554ccc180a55c13b213ec3c9e0019344722 Mon Sep 17 00:00:00 2001
From: Thomas Hipp <thomas.hipp at canonical.com>
Date: Tue, 3 Jul 2018 15:16:44 +0200
Subject: [PATCH 3/3] scripts: Remove lxc-to-lxd

Signed-off-by: Thomas Hipp <thomas.hipp at canonical.com>
---
 scripts/lxc-to-lxd | 641 -----------------------------------------------------
 1 file changed, 641 deletions(-)
 delete mode 100755 scripts/lxc-to-lxd

diff --git a/scripts/lxc-to-lxd b/scripts/lxc-to-lxd
deleted file mode 100755
index 4a93a4e16..000000000
--- a/scripts/lxc-to-lxd
+++ /dev/null
@@ -1,641 +0,0 @@
-#!/usr/bin/env python3
-import argparse
-import http.client
-import json
-import os
-import socket
-import subprocess
-import sys
-
-try:
-    import lxc
-except ImportError:
-    print("You must have python3-lxc installed for this script to work.")
-    sys.exit(1)
-
-
-# Whitelist of keys we either need to check or allow setting in LXD. The latter
-# is strictly only true for 'lxc.aa_profile'.
-keys_to_check = [
-    'lxc.pts',
-    # 'lxc.tty',
-    # 'lxc.devttydir',
-    # 'lxc.kmsg',
-    'lxc.aa_profile',
-    # 'lxc.cgroup.',
-    'lxc.loglevel',
-    # 'lxc.logfile',
-    'lxc.mount.auto',
-    'lxc.mount',
-    # 'lxc.rootfs.mount',
-    # 'lxc.rootfs.options',
-    # 'lxc.pivotdir',
-    # 'lxc.hook.pre-start',
-    # 'lxc.hook.pre-mount',
-    # 'lxc.hook.mount',
-    # 'lxc.hook.autodev',
-    # 'lxc.hook.start',
-    # 'lxc.hook.stop',
-    # 'lxc.hook.post-stop',
-    # 'lxc.hook.clone',
-    # 'lxc.hook.destroy',
-    # 'lxc.hook',
-    'lxc.network.type',
-    'lxc.network.flags',
-    'lxc.network.link',
-    'lxc.network.name',
-    'lxc.network.macvlan.mode',
-    'lxc.network.veth.pair',
-    # 'lxc.network.script.up',
-    # 'lxc.network.script.down',
-    'lxc.network.hwaddr',
-    'lxc.network.mtu',
-    # 'lxc.network.vlan.id',
-    # 'lxc.network.ipv4.gateway',
-    # 'lxc.network.ipv4',
-    # 'lxc.network.ipv6.gateway',
-    # 'lxc.network.ipv6',
-    # 'lxc.network.',
-    # 'lxc.network',
-    # 'lxc.console.logfile',
-    # 'lxc.console',
-    'lxc.include',
-    'lxc.start.auto',
-    'lxc.start.delay',
-    'lxc.start.order',
-    # 'lxc.monitor.unshare',
-    # 'lxc.group',
-    'lxc.environment',
-    # 'lxc.init_cmd',
-    # 'lxc.init_uid',
-    # 'lxc.init_gid',
-    # 'lxc.ephemeral',
-    # 'lxc.syslog',
-    # 'lxc.no_new_privs',
-
-    # Additional keys that are either set by this script or are used to report
-    # helpful errors to users.
-    'lxc.arch',
-    'lxc.id_map',
-    'lxd.migrated',
-    'lxc.rootfs.backend',
-    'lxc.rootfs',
-    'lxc.utsname',
-    'lxc.aa_allow_incomplete',
-    'lxc.autodev',
-    'lxc.haltsignal',
-    'lxc.rebootsignal',
-    'lxc.stopsignal',
-    'lxc.mount.entry',
-    'lxc.cap.drop',
-    # 'lxc.cap.keep',
-    'lxc.seccomp',
-    # 'lxc.se_context',
-    ]
-
-
-# Unix connection to LXD
-class UnixHTTPConnection(http.client.HTTPConnection):
-    def __init__(self, path):
-        http.client.HTTPConnection.__init__(self, 'localhost')
-        self.path = path
-
-    def connect(self):
-        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
-        sock.connect(self.path)
-        self.sock = sock
-
-
-# Fetch a config key as a list
-def config_get(config, key, default=None):
-    result = []
-    for line in config:
-        fields = line.split("=", 1)
-        if fields[0].strip() == key:
-            result.append(fields[-1].strip())
-
-    if len(result) == 0:
-        return default
-    else:
-        return result
-
-
-def config_keys(config):
-    keys = []
-    for line in config:
-        fields = line.split("=", 1)
-        cur = fields[0].strip()
-        if cur and not cur.startswith("#") and cur.startswith("lxc."):
-            keys.append(cur)
-
-    return keys
-
-
-# Parse a LXC configuration file, called recursively for includes
-def config_parse(path):
-    config = []
-    with open(path, "r") as fd:
-        for line in fd:
-            line = line.strip()
-            key = line.split("=", 1)[0].strip()
-            value = line.split("=", 1)[-1].strip()
-
-            # Parse user-added includes
-            if key == "lxc.include":
-                # Ignore our own default configs
-                if value.startswith("/usr/share/lxc/config/"):
-                    continue
-
-                if os.path.isfile(value):
-                    config += config_parse(value)
-                    continue
-                elif os.path.isdir(value):
-                    for entry in os.listdir(value):
-                        if not entry.endswith(".conf"):
-                            continue
-
-                        config += config_parse(os.path.join(value, entry))
-                    continue
-                else:
-                    print("Invalid include: %s", line)
-
-            # Expand any fstab
-            if key == "lxc.mount":
-                if not os.path.exists(value):
-                    print("Container fstab file doesn't exist, skipping...")
-                    continue
-
-                with open(value, "r") as fd:
-                    for line in fd:
-                        line = line.strip()
-                        if (line and not line.startswith("#") and
-                                line.startswith("lxc.")):
-                            config.append("lxc.mount.entry = %s" % line)
-                continue
-
-            # Process normal configuration keys
-            if line and not line.strip().startswith("#"):
-                config.append(line)
-
-    return config
-
-
-def container_exists(lxd_socket, container_name):
-    lxd = UnixHTTPConnection(lxd_socket)
-    lxd.request("GET", "/1.0/containers/%s" % container_name)
-    if lxd.getresponse().status == 404:
-        return False
-
-    return True
-
-
-def container_create(lxd_socket, args):
-    # Define the container
-    lxd = UnixHTTPConnection(lxd_socket)
-    lxd.request("POST", "/1.0/containers", json.dumps(args))
-    r = lxd.getresponse()
-
-    # Decode the response
-    resp = json.loads(r.read().decode())
-    if resp["type"] == "error":
-        raise Exception("Failed to define container: %s" % resp["error"])
-
-    # Wait for result
-    lxd = UnixHTTPConnection(lxd_socket)
-    lxd.request("GET", "%s/wait" % resp["operation"])
-    r = lxd.getresponse()
-
-    # Decode the response
-    resp = json.loads(r.read().decode())
-    if resp["type"] == "error":
-        raise Exception("Failed to define container: %s" % resp["error"])
-
-
-# Convert a LXC container to a LXD one
-def convert_container(lxd_socket, container_name, args):
-    print("==> Processing container: %s" % container_name)
-
-    # Load the container
-    try:
-        container = lxc.Container(container_name, args.lxcpath)
-    except Exception:
-        print("Invalid container configuration, skipping...")
-        return False
-
-    # As some keys can't be queried over the API, parse the config ourselves
-    print("Parsing LXC configuration")
-    lxc_config = config_parse(container.config_file_name)
-    found_keys = config_keys(lxc_config)
-
-    # Generic check for any invalid LXC configuration keys.
-    print("Checking for unsupported LXC configuration keys")
-    diff = list(set(found_keys) - set(keys_to_check))
-    for d in diff:
-        if (not d.startswith('lxc.network.') and not
-                d.startswith('lxc.cgroup.')):
-            print("Found at least one unsupported config key %s: " % d)
-            print("Not importing this container, skipping...")
-            return False
-
-    if args.debug:
-        print("Container configuration:")
-        print(" ", end="")
-        print("\n ".join(lxc_config))
-        print("")
-
-    # Check for keys that have values differing from the LXD defaults.
-    print("Checking whether container has already been migrated")
-    if config_get(lxc_config, "lxd.migrated"):
-        print("Container has already been migrated, skipping...")
-        return False
-
-    # Make sure we don't have a conflict
-    print("Checking for existing containers")
-    if container_exists(lxd_socket, container_name):
-        print("Container already exists, skipping...")
-        return False
-
-    # Validating lxc.id_map: must be unset.
-    print("Validating container mode")
-    if config_get(lxc_config, "lxc.id_map"):
-        print("Unprivileged containers aren't supported, skipping...")
-        return False
-
-    # Validate lxc.utsname
-    print("Validating container name")
-    value = config_get(lxc_config, "lxc.utsname")
-    if value and value[0] != container_name:
-        print("Container name doesn't match lxc.utsname, skipping...")
-        return False
-
-    # Validate lxc.aa_allow_incomplete: must be set to 0 or unset.
-    print("Validating whether incomplete AppArmor support is enabled")
-    value = config_get(lxc_config, "lxc.aa_allow_incomplete")
-    if value and int(value[0]) != 0:
-        print("Container allows incomplete AppArmor support, skipping...")
-        return False
-
-    # Validate lxc.autodev: must be set to 1 or unset.
-    print("Validating whether mounting a minimal /dev is enabled")
-    value = config_get(lxc_config, "lxc.autodev")
-    if value and int(value[0]) != 1:
-        print("Container doesn't mount a minimal /dev filesystem, skipping...")
-        return False
-
-    # Validate lxc.haltsignal: must be unset.
-    print("Validating that no custom haltsignal is set")
-    value = config_get(lxc_config, "lxc.haltsignal")
-    if value:
-        print("Container sets custom halt signal, skipping...")
-        return False
-
-    # Validate lxc.rebootsignal: must be unset.
-    print("Validating that no custom rebootsignal is set")
-    value = config_get(lxc_config, "lxc.rebootsignal")
-    if value:
-        print("Container sets custom reboot signal, skipping...")
-        return False
-
-    # Validate lxc.stopsignal: must be unset.
-    print("Validating that no custom stopsignal is set")
-    value = config_get(lxc_config, "lxc.stopsignal")
-    if value:
-        print("Container sets custom stop signal, skipping...")
-        return False
-
-    # Extract and valid rootfs key
-    print("Validating container rootfs")
-    value = config_get(lxc_config, "lxc.rootfs")
-    if not value:
-        print("Invalid container, missing lxc.rootfs key, skipping...")
-        return False
-
-    rootfs = value[0]
-
-    if not os.path.exists(rootfs):
-        print("Couldn't find the container rootfs '%s', skipping..." % rootfs)
-        return False
-
-    # Base config
-    config = {}
-    config['security.privileged'] = "true"
-    devices = {}
-    devices['eth0'] = {'type': "none"}
-
-    # Convert network configuration
-    print("Processing network configuration")
-    try:
-        count = len(container.get_config_item("lxc.network"))
-    except Exception:
-        count = 0
-
-    for i in range(count):
-        device = {"type": "nic"}
-
-        # Get the device type
-        device["nictype"] = container.get_config_item("lxc.network")[i]
-
-        # Get everything else
-        dev = container.network[i]
-
-        # Validate configuration
-        if dev.ipv4 or dev.ipv4_gateway:
-            print("IPv4 network configuration isn't supported, skipping...")
-            return False
-
-        if dev.ipv6 or dev.ipv6_gateway:
-            print("IPv6 network configuration isn't supported, skipping...")
-            return False
-
-        if dev.script_up or dev.script_down:
-            print("Network config scripts aren't supported, skipping...")
-            return False
-
-        if device["nictype"] == "none":
-            print("\"none\" network mode isn't supported, skipping...")
-            return False
-
-        if device["nictype"] == "vlan":
-            print("\"vlan\" network mode isn't supported, skipping...")
-            return False
-
-        # Convert the configuration
-        if dev.hwaddr:
-            device['hwaddr'] = dev.hwaddr
-
-        if dev.link:
-            device['parent'] = dev.link
-
-        if dev.mtu:
-            device['mtu'] = dev.mtu
-
-        if dev.name:
-            device['name'] = dev.name
-
-        if dev.veth_pair:
-            device['host_name'] = dev.veth_pair
-
-        if device["nictype"] == "veth":
-            if "parent" in device:
-                device["nictype"] = "bridged"
-            else:
-                device["nictype"] = "p2p"
-
-        if device["nictype"] == "phys":
-            device["nictype"] = "physical"
-
-        if device["nictype"] == "empty":
-            continue
-
-        devices['convert_net%d' % i] = device
-        count += 1
-
-    # Convert storage configuration
-    value = config_get(lxc_config, "lxc.mount.entry", [])
-    i = 0
-    for entry in value:
-        mount = entry.split(" ")
-        if len(mount) < 4:
-            print("Invalid mount configuration, skipping...")
-            return False
-
-        # Ignore mounts that are present in LXD containers by default.
-        if mount[0] in ("proc", "sysfs"):
-            continue
-
-        device = {'type': "disk"}
-
-        # Deal with read-only mounts
-        if "ro" in mount[3].split(","):
-            device['readonly'] = "true"
-
-        # Deal with optional mounts
-        if "optional" in mount[3].split(","):
-            device['optional'] = "true"
-        elif not os.path.exists(mount[0]):
-            print("Invalid mount configuration, source path doesn't exist.")
-            return False
-
-        # Set the source
-        device['source'] = mount[0]
-
-        # Figure out the target
-        if mount[1][0] != "/":
-            device['path'] = "/%s" % mount[1]
-        else:
-            device['path'] = mount[1].split(rootfs, 1)[-1]
-
-        devices['convert_mount%d' % i] = device
-        i += 1
-
-    # Convert environment
-    print("Processing environment configuration")
-    value = config_get(lxc_config, "lxc.environment", [])
-    for env in value:
-        entry = env.split("=", 1)
-        config['environment.%s' % entry[0].strip()] = entry[-1].strip()
-
-    # Convert auto-start
-    print("Processing container boot configuration")
-    value = config_get(lxc_config, "lxc.start.auto")
-    if value and int(value[0]) > 0:
-        config['boot.autostart'] = "true"
-
-    value = config_get(lxc_config, "lxc.start.delay")
-    if value and int(value[0]) > 0:
-        config['boot.autostart.delay'] = value[0]
-
-    value = config_get(lxc_config, "lxc.start.order")
-    if value and int(value[0]) > 0:
-        config['boot.autostart.priority'] = value[0]
-
-    # Convert apparmor
-    print("Processing container apparmor configuration")
-    value = config_get(lxc_config, "lxc.aa_profile")
-    if value:
-        if value[0] == "lxc-container-default-with-nesting":
-            config['security.nesting'] = "true"
-        elif value[0] != "lxc-container-default":
-            config["raw.lxc"] = "lxc.aa_profile=%s" % value[0]
-
-    # Convert seccomp
-    print("Processing container seccomp configuration")
-    value = config_get(lxc_config, "lxc.seccomp")
-    if value and value[0] != "/usr/share/lxc/config/common.seccomp":
-        print("Custom seccomp profiles aren't supported, skipping...")
-        return False
-
-    # Convert SELinux
-    print("Processing container SELinux configuration")
-    value = config_get(lxc_config, "lxc.se_context")
-    if value:
-        print("Custom SELinux policies aren't supported, skipping...")
-        return False
-
-    # Convert capabilities
-    print("Processing container capabilities configuration")
-    value = config_get(lxc_config, "lxc.cap.drop")
-    if value:
-        for cap in value:
-            # Ignore capabilities that are dropped in LXD containers by default.
-            if cap in ("mac_admin", "mac_override", "sys_module", "sys_time"):
-                continue
-            print("Custom capabilities aren't supported, skipping...")
-            return False
-
-    value = config_get(lxc_config, "lxc.cap.keep")
-    if value:
-        print("Custom capabilities aren't supported, skipping...")
-        return False
-
-    # Setup the container creation request
-    new = {'name': container_name,
-           'source': {'type': 'none'},
-           'config': config,
-           'devices': devices,
-           'profiles': ["default"]}
-
-    # Set the container architecture if set in LXC
-    print("Processing container architecture configuration")
-    arches = {'i686': "i686",
-              'x86_64': "x86_64",
-              'armhf': "armv7l",
-              'arm64': "aarch64",
-              'powerpc': "ppc",
-              'powerpc64': "ppc64",
-              'ppc64el': "ppc64le",
-              's390x': "s390x"}
-
-    arch = None
-    try:
-        arch = config_get(lxc_config, "lxc.arch", None)
-
-        if arch and arch[0] in arches:
-            new['architecture'] = arches[arch[0]]
-        else:
-            print("Unknown architecture, assuming native.")
-    except Exception:
-        print("Couldn't find container architecture, assuming native.")
-
-    # Define the container in LXD
-    if args.debug:
-        print("LXD container config:")
-        print(json.dumps(new, indent=True, sort_keys=True))
-
-    if args.dry_run:
-        return True
-
-    if container.running:
-        print("Only stopped containers can be migrated, skipping...")
-        return False
-
-    try:
-        print("Creating the container")
-        container_create(lxd_socket, new)
-    except Exception as e:
-        raise
-        print("Failed to create the container: %s" % e)
-        return False
-
-    # Transfer the filesystem
-    lxd_rootfs = os.path.join(args.lxdpath, "containers",
-                              container_name, "rootfs")
-
-    if args.move_rootfs:
-        if os.path.exists(lxd_rootfs):
-            os.rmdir(lxd_rootfs)
-
-        if subprocess.call(["mv", rootfs, lxd_rootfs]) != 0:
-            print("Failed to move the container rootfs, skipping...")
-            return False
-
-        os.mkdir(rootfs)
-    else:
-        print("Copying container rootfs")
-        if not os.path.exists(lxd_rootfs):
-            os.mkdir(lxd_rootfs)
-
-        if subprocess.call(["rsync", "-Aa", "--sparse",
-                            "--acls", "--numeric-ids", "--hard-links",
-                            "%s/" % rootfs, "%s/" % lxd_rootfs]) != 0:
-            print("Failed to transfer the container rootfs, skipping...")
-            return False
-
-    # Delete the source
-    if args.delete:
-        print("Deleting source container")
-        container.delete()
-
-    # Mark the container as migrated
-    with open(container.config_file_name, "a") as fd:
-        fd.write("lxd.migrated=true\n")
-    print("Container is ready to use")
-    return True
-
-
-# Argument parsing
-parser = argparse.ArgumentParser()
-parser.add_argument("--dry-run", action="store_true", default=False,
-                    help="Dry run mode")
-parser.add_argument("--debug", action="store_true", default=False,
-                    help="Print debugging output")
-parser.add_argument("--all", action="store_true", default=False,
-                    help="Import all containers")
-parser.add_argument("--delete", action="store_true", default=False,
-                    help="Delete the source container")
-parser.add_argument("--move-rootfs", action="store_true", default=False,
-                    help="Move the container rootfs rather than copying it")
-parser.add_argument("--lxcpath", type=str, default=False,
-                    help="Alternate LXC path")
-parser.add_argument("--lxdpath", type=str, default="/var/lib/lxd",
-                    help="Alternate LXD path")
-parser.add_argument(dest='containers', metavar="CONTAINER", type=str,
-                    help="Container to import", nargs="*")
-args = parser.parse_args()
-
-# Sanity checks
-if not os.geteuid() == 0:
-    parser.error("You must be root to run this tool")
-
-if (not args.containers and not args.all) or (args.containers and args.all):
-    parser.error("You must either pass container names or --all")
-
-# Connect to LXD
-if 'LXD_SOCKET' in os.environ:
-    lxd_socket = os.environ['LXD_SOCKET']
-else:
-    lxd_socket = os.path.join(args.lxdpath, "unix.socket")
-
-if not os.path.exists(lxd_socket):
-    print("LXD isn't running.")
-    sys.exit(1)
-
-# Run migration
-results = {}
-count = 0
-for container_name in lxc.list_containers(config_path=args.lxcpath):
-    if args.containers and container_name not in args.containers:
-        continue
-
-    if count > 0:
-        print("")
-
-    results[container_name] = convert_container(lxd_socket,
-                                                container_name, args)
-    count += 1
-
-# Print summary
-if not results:
-    print("No container to migrate")
-    sys.exit(0)
-
-print("")
-print("==> Migration summary")
-for name, result in results.items():
-    if result:
-        print("%s: SUCCESS" % name)
-    else:
-        print("%s: FAILURE" % name)
-
-if False in results.values():
-    sys.exit(1)


More information about the lxc-devel mailing list