[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