[lxc-devel] [lxd/master] Port lxd command to cobra

stgraber on Github lxc-bot at linuxcontainers.org
Wed Mar 21 05:23:53 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/20180321/b5bd6abc/attachment.bin>
-------------- next part --------------
From d5c5a2d53d39043f4282f703e7c32dc137fdd4e9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 27 Feb 2018 11:03:15 -0500
Subject: [PATCH 01/19] lxc/list: Make --fast much faster
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxc/list.go | 34 ++++++++++++++++++++++------------
 1 file changed, 22 insertions(+), 12 deletions(-)

diff --git a/lxc/list.go b/lxc/list.go
index ecd2e46ac..ad487b2c5 100644
--- a/lxc/list.go
+++ b/lxc/list.go
@@ -14,6 +14,7 @@ import (
 	"github.com/olekukonko/tablewriter"
 	"gopkg.in/yaml.v2"
 
+	"github.com/lxc/lxd/client"
 	"github.com/lxc/lxd/lxc/config"
 	"github.com/lxc/lxd/shared"
 	"github.com/lxc/lxd/shared/api"
@@ -244,18 +245,22 @@ func (c *listCmd) listContainers(conf *config.Config, remote string, cinfos []ap
 	for i := 0; i < threads; i++ {
 		cStatesWg.Add(1)
 		go func() {
-			d, err := conf.GetContainerServer(remote)
-			if err != nil {
-				cStatesWg.Done()
-				return
-			}
-
+			var d lxd.ContainerServer
+			var err error
 			for {
 				cName, more := <-cStatesQueue
 				if !more {
 					break
 				}
 
+				if d == nil {
+					d, err = conf.GetContainerServer(remote)
+					if err != nil {
+						cStatesWg.Done()
+						return
+					}
+				}
+
 				state, _, err := d.GetContainerState(cName)
 				if err != nil {
 					continue
@@ -270,18 +275,23 @@ func (c *listCmd) listContainers(conf *config.Config, remote string, cinfos []ap
 
 		cSnapshotsWg.Add(1)
 		go func() {
-			d, err := conf.GetContainerServer(remote)
-			if err != nil {
-				cSnapshotsWg.Done()
-				return
-			}
-
+			var d lxd.ContainerServer
+			var err error
 			for {
 				cName, more := <-cSnapshotsQueue
 				if !more {
 					break
 				}
 
+				if d == nil {
+					d, err = conf.GetContainerServer(remote)
+					if err != nil {
+						cSnapshotsWg.Done()
+						return
+					}
+
+				}
+
 				snaps, err := d.GetContainerSnapshots(cName)
 				if err != nil {
 					continue

From 4daa6c8241c8841483781e37690008267fab160c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Sun, 25 Feb 2018 23:14:38 -0500
Subject: [PATCH 02/19] lxd-benchmark: Simplify using setup/teardown
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd-benchmark/main.go        | 15 +++++++++------
 lxd-benchmark/main_delete.go | 12 +-----------
 lxd-benchmark/main_init.go   | 12 +-----------
 lxd-benchmark/main_launch.go | 12 +-----------
 lxd-benchmark/main_start.go  | 12 +-----------
 lxd-benchmark/main_stop.go   | 12 +-----------
 6 files changed, 14 insertions(+), 61 deletions(-)

diff --git a/lxd-benchmark/main.go b/lxd-benchmark/main.go
index 64c010f8f..dcaca9778 100644
--- a/lxd-benchmark/main.go
+++ b/lxd-benchmark/main.go
@@ -19,11 +19,12 @@ type cmdGlobal struct {
 	flagReportLabel string
 	flagVersion     bool
 
-	srv    lxd.ContainerServer
-	report *benchmark.CSVReport
+	srv            lxd.ContainerServer
+	report         *benchmark.CSVReport
+	reportDuration time.Duration
 }
 
-func (c *cmdGlobal) Setup() error {
+func (c *cmdGlobal) Run(cmd *cobra.Command, args []string) error {
 	// Connect to LXD
 	srv, err := lxd.ConnectLXDUnix("", nil)
 	if err != nil {
@@ -48,18 +49,18 @@ func (c *cmdGlobal) Setup() error {
 	return nil
 }
 
-func (c *cmdGlobal) Teardown(action string, duration time.Duration) error {
+func (c *cmdGlobal) Teardown(cmd *cobra.Command, args []string) error {
 	// Nothing to do with not reporting
 	if c.report == nil {
 		return nil
 	}
 
-	label := action
+	label := cmd.Name()
 	if c.flagReportLabel != "" {
 		label = c.flagReportLabel
 	}
 
-	c.report.AddRecord(label, duration)
+	c.report.AddRecord(label, c.reportDuration)
 
 	err := c.report.Write()
 	if err != nil {
@@ -96,6 +97,8 @@ func main() {
 
 	// Global flags
 	globalCmd := cmdGlobal{}
+	app.PersistentPreRunE = globalCmd.Run
+	app.PersistentPostRunE = globalCmd.Teardown
 	app.PersistentFlags().BoolVar(&globalCmd.flagVersion, "version", false, "Print version number")
 	app.PersistentFlags().BoolVarP(&globalCmd.flagHelp, "help", "h", false, "Print help")
 	app.PersistentFlags().IntVarP(&globalCmd.flagParallel, "parallel", "P", -1, "Number of threads to use"+"``")
diff --git a/lxd-benchmark/main_delete.go b/lxd-benchmark/main_delete.go
index 5b0ed64c3..f632a73fa 100644
--- a/lxd-benchmark/main_delete.go
+++ b/lxd-benchmark/main_delete.go
@@ -22,12 +22,6 @@ func (c *cmdDelete) Command() *cobra.Command {
 }
 
 func (c *cmdDelete) Run(cmd *cobra.Command, args []string) error {
-	// Run shared setup code
-	err := c.global.Setup()
-	if err != nil {
-		return err
-	}
-
 	// Get the containers
 	containers, err := benchmark.GetContainers(c.global.srv)
 	if err != nil {
@@ -40,11 +34,7 @@ func (c *cmdDelete) Run(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	// Run shared reporting and teardown code
-	err = c.global.Teardown("delete", duration)
-	if err != nil {
-		return err
-	}
+	c.global.reportDuration = duration
 
 	return nil
 }
diff --git a/lxd-benchmark/main_init.go b/lxd-benchmark/main_init.go
index b7ef8d30d..59553aef0 100644
--- a/lxd-benchmark/main_init.go
+++ b/lxd-benchmark/main_init.go
@@ -27,12 +27,6 @@ func (c *cmdInit) Command() *cobra.Command {
 }
 
 func (c *cmdInit) Run(cmd *cobra.Command, args []string) error {
-	// Run shared setup code
-	err := c.global.Setup()
-	if err != nil {
-		return err
-	}
-
 	// Choose the image
 	image := "ubuntu:"
 	if len(args) > 0 {
@@ -45,11 +39,7 @@ func (c *cmdInit) Run(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	// Run shared reporting and teardown code
-	err = c.global.Teardown("init", duration)
-	if err != nil {
-		return err
-	}
+	c.global.reportDuration = duration
 
 	return nil
 }
diff --git a/lxd-benchmark/main_launch.go b/lxd-benchmark/main_launch.go
index 419d16fca..e7221e681 100644
--- a/lxd-benchmark/main_launch.go
+++ b/lxd-benchmark/main_launch.go
@@ -27,12 +27,6 @@ func (c *cmdLaunch) Command() *cobra.Command {
 }
 
 func (c *cmdLaunch) Run(cmd *cobra.Command, args []string) error {
-	// Run shared setup code
-	err := c.global.Setup()
-	if err != nil {
-		return err
-	}
-
 	// Choose the image
 	image := "ubuntu:"
 	if len(args) > 0 {
@@ -45,11 +39,7 @@ func (c *cmdLaunch) Run(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	// Run shared reporting and teardown code
-	err = c.global.Teardown("launch", duration)
-	if err != nil {
-		return err
-	}
+	c.global.reportDuration = duration
 
 	return nil
 }
diff --git a/lxd-benchmark/main_start.go b/lxd-benchmark/main_start.go
index b6d9655c1..97b4d7ff0 100644
--- a/lxd-benchmark/main_start.go
+++ b/lxd-benchmark/main_start.go
@@ -22,12 +22,6 @@ func (c *cmdStart) Command() *cobra.Command {
 }
 
 func (c *cmdStart) Run(cmd *cobra.Command, args []string) error {
-	// Run shared setup code
-	err := c.global.Setup()
-	if err != nil {
-		return err
-	}
-
 	// Get the containers
 	containers, err := benchmark.GetContainers(c.global.srv)
 	if err != nil {
@@ -40,11 +34,7 @@ func (c *cmdStart) Run(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	// Run shared reporting and teardown code
-	err = c.global.Teardown("start", duration)
-	if err != nil {
-		return err
-	}
+	c.global.reportDuration = duration
 
 	return nil
 }
diff --git a/lxd-benchmark/main_stop.go b/lxd-benchmark/main_stop.go
index f41f42f67..be6313e1f 100644
--- a/lxd-benchmark/main_stop.go
+++ b/lxd-benchmark/main_stop.go
@@ -22,12 +22,6 @@ func (c *cmdStop) Command() *cobra.Command {
 }
 
 func (c *cmdStop) Run(cmd *cobra.Command, args []string) error {
-	// Run shared setup code
-	err := c.global.Setup()
-	if err != nil {
-		return err
-	}
-
 	// Get the containers
 	containers, err := benchmark.GetContainers(c.global.srv)
 	if err != nil {
@@ -40,11 +34,7 @@ func (c *cmdStop) Run(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	// Run shared reporting and teardown code
-	err = c.global.Teardown("stop", duration)
-	if err != nil {
-		return err
-	}
+	c.global.reportDuration = duration
 
 	return nil
 }

From f077e9ddf74a99d7a5e19e8672bce2052e033413 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 27 Feb 2018 01:32:04 -0500
Subject: [PATCH 03/19] lxd: Remove the ready subcommand and API
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/api_internal.go |  6 +-----
 lxd/main.go         |  1 -
 lxd/main_ready.go   | 19 -------------------
 3 files changed, 1 insertion(+), 25 deletions(-)
 delete mode 100644 lxd/main_ready.go

diff --git a/lxd/api_internal.go b/lxd/api_internal.go
index 2db81f756..c97326a63 100644
--- a/lxd/api_internal.go
+++ b/lxd/api_internal.go
@@ -33,10 +33,6 @@ var apiInternal = []Command{
 	internalClusterContainerMovedCmd,
 }
 
-func internalReady(d *Daemon, r *http.Request) Response {
-	return InternalError(fmt.Errorf("The server does not support setup mode"))
-}
-
 func internalWaitReady(d *Daemon, r *http.Request) Response {
 	<-d.readyChan
 
@@ -162,7 +158,7 @@ func internalSQL(d *Daemon, r *http.Request) Response {
 }
 
 var internalShutdownCmd = Command{name: "shutdown", put: internalShutdown}
-var internalReadyCmd = Command{name: "ready", put: internalReady, get: internalWaitReady}
+var internalReadyCmd = Command{name: "ready", get: internalWaitReady}
 var internalContainerOnStartCmd = Command{name: "containers/{id}/onstart", get: internalContainerOnStart}
 var internalContainerOnStopCmd = Command{name: "containers/{id}/onstop", get: internalContainerOnStop}
 var internalSQLCmd = Command{name: "sql", post: internalSQL}
diff --git a/lxd/main.go b/lxd/main.go
index a257f8fc3..44e579e5c 100644
--- a/lxd/main.go
+++ b/lxd/main.go
@@ -52,7 +52,6 @@ var subcommands = map[string]SubCommand{
 	"daemon":           cmdDaemon,
 	"callhook":         cmdCallHook,
 	"init":             cmdInit,
-	"ready":            cmdReady,
 	"shutdown":         cmdShutdown,
 	"waitready":        cmdWaitReady,
 	"import":           cmdImport,
diff --git a/lxd/main_ready.go b/lxd/main_ready.go
deleted file mode 100644
index 98f8ba634..000000000
--- a/lxd/main_ready.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package main
-
-import (
-	"github.com/lxc/lxd/client"
-)
-
-func cmdReady(args *Args) error {
-	c, err := lxd.ConnectLXDUnix("", nil)
-	if err != nil {
-		return err
-	}
-
-	_, _, err = c.RawQuery("PUT", "/internal/ready", nil, "")
-	if err != nil {
-		return err
-	}
-
-	return nil
-}

From 231f03d5cc642f8038d5e562323531ff703af82d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Sun, 25 Feb 2018 23:10:42 -0500
Subject: [PATCH 04/19] lxd: Port to cobra
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/daemon.go                  |   8 +--
 lxd/main.go                    | 139 +++++++++++++++++++++++++++--------------
 lxd/main_activateifneeded.go   |  29 ++++++++-
 lxd/main_args_test.go          |  98 -----------------------------
 lxd/main_callhook.go           |  48 +++++++++++---
 lxd/main_daemon.go             |  55 +++++++++++++---
 lxd/main_import.go             |  52 ++++++++++++---
 lxd/main_migratedumpsuccess.go |  44 ++++++++++---
 lxd/main_netcat.go             |  58 ++++++++++++-----
 lxd/main_shutdown.go           |  44 ++++++++++---
 lxd/main_sql.go                |  49 ++++++++++++---
 lxd/main_waitready.go          |  51 ++++++++++-----
 test/suites/profiling.sh       |   4 +-
 13 files changed, 453 insertions(+), 226 deletions(-)
 delete mode 100644 lxd/main_args_test.go

diff --git a/lxd/daemon.go b/lxd/daemon.go
index 7c00809d4..0d21455e7 100644
--- a/lxd/daemon.go
+++ b/lxd/daemon.go
@@ -77,9 +77,9 @@ type externalAuth struct {
 
 // DaemonConfig holds configuration values for Daemon.
 type DaemonConfig struct {
-	Group       string  // Group name the local unix socket should be chown'ed to
-	Trace       string  // Comma separated list of sub-systems to trace
-	RaftLatency float64 // Coarse grain measure of the cluster latency
+	Group       string   // Group name the local unix socket should be chown'ed to
+	Trace       []string // List of sub-systems to trace
+	RaftLatency float64  // Coarse grain measure of the cluster latency
 }
 
 // NewDaemon returns a new Daemon object with the given configuration.
@@ -389,7 +389,7 @@ func (d *Daemon) init() error {
 	}
 
 	/* List of sub-systems to trace */
-	trace := strings.Split(d.config.Trace, ",")
+	trace := d.config.Trace
 
 	/* Initialize the operating system facade */
 	err = d.os.Init()
diff --git a/lxd/main.go b/lxd/main.go
index 44e579e5c..9673628e5 100644
--- a/lxd/main.go
+++ b/lxd/main.go
@@ -5,65 +5,112 @@ import (
 	"os"
 	"time"
 
-	"github.com/lxc/lxd/shared/cmd"
+	"github.com/spf13/cobra"
+
+	"github.com/lxc/lxd/shared/logger"
+	"github.com/lxc/lxd/shared/logging"
+	"github.com/lxc/lxd/shared/version"
 )
 
 // Global variables
 var debug bool
 var verbose bool
 
+// Initialize the random number generator
 func init() {
 	rand.Seed(time.Now().UTC().UnixNano())
 }
 
-func main() {
-	// Pupulate a new Args instance by parsing the command line arguments
-	// passed.
-	context := cmd.DefaultContext()
-	args := &Args{}
-	parser := cmd.NewParser(context, usage)
-	parser.Parse(os.Args, args)
-
-	// Set the global variables
-	debug = args.Debug
-	verbose = args.Verbose
-
-	// Process sub-commands
-	subcommand := cmdDaemon // default sub-command if none is specified
-	if args.Subcommand != "" {
-		subcommand, _ = subcommands[args.Subcommand]
+type cmdGlobal struct {
+	flagHelp    bool
+	flagVersion bool
+
+	flagLogFile    string
+	flagLogDebug   bool
+	flagLogSyslog  bool
+	flagLogTrace   []string
+	flagLogVerbose bool
+}
+
+func (c *cmdGlobal) Run(cmd *cobra.Command, args []string) error {
+	// Set logging global variables
+	debug = c.flagLogVerbose
+	verbose = c.flagLogDebug
+
+	// Setup logger
+	syslog := ""
+	if c.flagLogSyslog {
+		syslog = "lxd"
 	}
-	if subcommand == nil {
-		context.Output(usage)
-		context.Error("error: Unknown arguments\n")
-		os.Exit(1)
+
+	log, err := logging.GetLogger(syslog, c.flagLogFile, c.flagLogVerbose, c.flagLogDebug, nil)
+	if err != nil {
+		return err
 	}
+	logger.Log = log
 
-	os.Exit(RunSubCommand(subcommand, context, args, eventsHandler{}))
+	return nil
 }
 
-// Index of SubCommand functions by command line name
-//
-// "forkputfile", "forkgetfile", "forkmount" and "forkumount" are handled specially in main_nsexec.go
-// "forkgetnet" is partially handled in main_nsexec.go (setns)
-var subcommands = map[string]SubCommand{
-	// Main commands
-	"activateifneeded": cmdActivateIfNeeded,
-	"daemon":           cmdDaemon,
-	"callhook":         cmdCallHook,
-	"init":             cmdInit,
-	"shutdown":         cmdShutdown,
-	"waitready":        cmdWaitReady,
-	"import":           cmdImport,
-
-	// Internal commands
-	"forkconsole":        cmdForkConsole,
-	"forkgetnet":         cmdForkGetNet,
-	"forkmigrate":        cmdForkMigrate,
-	"forkstart":          cmdForkStart,
-	"forkexec":           cmdForkExec,
-	"netcat":             cmdNetcat,
-	"migratedumpsuccess": cmdMigrateDumpSuccess,
-	"forkproxy":          cmdProxyDevStart,
-	"sql":                cmdSQL,
+func main() {
+	// daemon command (main)
+	daemonCmd := cmdDaemon{}
+	app := daemonCmd.Command()
+	app.SilenceUsage = true
+
+	// Workaround for main command
+	app.Args = cobra.ArbitraryArgs
+
+	// Global flags
+	globalCmd := cmdGlobal{}
+	daemonCmd.global = &globalCmd
+	app.PersistentPreRunE = globalCmd.Run
+	app.PersistentFlags().BoolVar(&globalCmd.flagVersion, "version", false, "Print version number")
+	app.PersistentFlags().BoolVarP(&globalCmd.flagHelp, "help", "h", false, "Print help")
+	app.PersistentFlags().StringVar(&globalCmd.flagLogFile, "logfile", "", "Path to the log file"+"``")
+	app.PersistentFlags().StringArrayVar(&globalCmd.flagLogTrace, "trace", []string{}, "Log tracing targets"+"``")
+	app.PersistentFlags().BoolVarP(&globalCmd.flagLogDebug, "debug", "d", false, "Show all debug messages")
+	app.PersistentFlags().BoolVarP(&globalCmd.flagLogVerbose, "verbose", "v", false, "Show all information messages")
+
+	// Version handling
+	app.SetVersionTemplate("{{.Version}}\n")
+	app.Version = version.Version
+
+	// activateifneeded sub-command
+	activateifneededCmd := cmdActivateifneeded{global: &globalCmd}
+	app.AddCommand(activateifneededCmd.Command())
+
+	// callhook sub-command
+	callhookCmd := cmdCallhook{global: &globalCmd}
+	app.AddCommand(callhookCmd.Command())
+
+	// import sub-command
+	importCmd := cmdImport{global: &globalCmd}
+	app.AddCommand(importCmd.Command())
+
+	// migratedumpsuccess sub-command
+	migratedumpsuccessCmd := cmdMigratedumpsuccess{global: &globalCmd}
+	app.AddCommand(migratedumpsuccessCmd.Command())
+
+	// netcat sub-command
+	netcatCmd := cmdNetcat{global: &globalCmd}
+	app.AddCommand(netcatCmd.Command())
+
+	// shutdown sub-command
+	shutdownCmd := cmdShutdown{global: &globalCmd}
+	app.AddCommand(shutdownCmd.Command())
+
+	// sql sub-command
+	sqlCmd := cmdSql{global: &globalCmd}
+	app.AddCommand(sqlCmd.Command())
+
+	// waitready sub-command
+	waitreadyCmd := cmdWaitready{global: &globalCmd}
+	app.AddCommand(waitreadyCmd.Command())
+
+	// Run the main command and handle errors
+	err := app.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
 }
diff --git a/lxd/main_activateifneeded.go b/lxd/main_activateifneeded.go
index 3e654f35b..25b2b2b7c 100644
--- a/lxd/main_activateifneeded.go
+++ b/lxd/main_activateifneeded.go
@@ -7,6 +7,8 @@ import (
 	"path/filepath"
 
 	"github.com/CanonicalLtd/go-sqlite3"
+	"github.com/spf13/cobra"
+
 	"github.com/lxc/lxd/client"
 	"github.com/lxc/lxd/lxd/db"
 	"github.com/lxc/lxd/lxd/node"
@@ -19,7 +21,32 @@ func init() {
 	sql.Register("dqlite_direct_access", &sqlite3.SQLiteDriver{ConnectHook: sqliteDirectAccess})
 }
 
-func cmdActivateIfNeeded(args *Args) error {
+type cmdActivateifneeded struct {
+	cmd    *cobra.Command
+	global *cmdGlobal
+}
+
+func (c *cmdActivateifneeded) Command() *cobra.Command {
+	cmd := &cobra.Command{}
+	cmd.Use = "activateifneeded"
+	cmd.Short = "Check if LXD should be started"
+	cmd.Long = `Description:
+  Check if LXD should be started
+
+  This command will check if LXD has any auto-started containers,
+  containers which were running prior to LXD's last shutdown or if it's
+  configured to listen on the network address.
+
+  If any of those is true, then a connection will be attempted to the
+  LXD socket which will cause a socket-activated LXD to be spawned.
+`
+	cmd.RunE = c.Run
+
+	c.cmd = cmd
+	return cmd
+}
+
+func (c *cmdActivateifneeded) Run(cmd *cobra.Command, args []string) error {
 	// Only root should run this
 	if os.Geteuid() != 0 {
 		return fmt.Errorf("This must be run as root")
diff --git a/lxd/main_args_test.go b/lxd/main_args_test.go
deleted file mode 100644
index 3dc5cd166..000000000
--- a/lxd/main_args_test.go
+++ /dev/null
@@ -1,98 +0,0 @@
-package main
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-
-	"github.com/lxc/lxd/shared/cmd"
-)
-
-// Check the default values of all command line arguments.
-func TestParse_ArgsDefaults(t *testing.T) {
-	context := cmd.NewMemoryContext(cmd.NewMemoryStreams(""))
-	line := []string{"lxd"}
-	args := &Args{}
-	parser := cmd.NewParser(context, "")
-	parser.Parse(line, args)
-
-	assert.Equal(t, false, args.Auto)
-	assert.Equal(t, false, args.Preseed)
-	assert.Equal(t, "", args.CPUProfile)
-	assert.Equal(t, false, args.Debug)
-	assert.Equal(t, "", args.Trace)
-	assert.Equal(t, "", args.Group)
-	assert.Equal(t, false, args.Help)
-	assert.Equal(t, "", args.Logfile)
-	assert.Equal(t, "", args.MemProfile)
-	assert.Equal(t, "", args.NetworkAddress)
-	assert.Equal(t, int64(-1), args.NetworkPort)
-	assert.Equal(t, -1, args.PrintGoroutinesEvery)
-	assert.Equal(t, "", args.StorageBackend)
-	assert.Equal(t, "", args.StorageCreateDevice)
-	assert.Equal(t, int64(-1), args.StorageCreateLoop)
-	assert.Equal(t, "", args.StorageDataset)
-	assert.Equal(t, false, args.Syslog)
-	assert.Equal(t, -1, args.Timeout)
-	assert.Equal(t, "", args.TrustPassword)
-	assert.Equal(t, false, args.Verbose)
-	assert.Equal(t, false, args.Version)
-	assert.Equal(t, false, args.Force)
-}
-
-// Check that parsing the command line results in the correct attributes
-// being set.
-func TestParse_ArgsCustom(t *testing.T) {
-	context := cmd.NewMemoryContext(cmd.NewMemoryStreams(""))
-	line := []string{
-		"lxd",
-		"--auto",
-		"--preseed",
-		"--cpuprofile", "lxd.cpu",
-		"--debug",
-		"--trace", "dqlite,raft",
-		"--group", "lxd",
-		"--help",
-		"--logfile", "lxd.log",
-		"--memprofile", "lxd.mem",
-		"--network-address", "127.0.0.1",
-		"--network-port", "666",
-		"--print-goroutines-every", "10",
-		"--storage-backend", "btrfs",
-		"--storage-create-device", "/dev/sda2",
-		"--storage-create-loop", "8192",
-		"--storage-pool", "default",
-		"--syslog",
-		"--timeout", "30",
-		"--trust-password", "sekret",
-		"--verbose",
-		"--version",
-		"--force",
-	}
-	args := &Args{}
-	parser := cmd.NewParser(context, "")
-	parser.Parse(line, args)
-
-	assert.Equal(t, true, args.Auto)
-	assert.Equal(t, true, args.Preseed)
-	assert.Equal(t, "lxd.cpu", args.CPUProfile)
-	assert.Equal(t, true, args.Debug)
-	assert.Equal(t, "dqlite,raft", args.Trace)
-	assert.Equal(t, "lxd", args.Group)
-	assert.Equal(t, true, args.Help)
-	assert.Equal(t, "lxd.log", args.Logfile)
-	assert.Equal(t, "lxd.mem", args.MemProfile)
-	assert.Equal(t, "127.0.0.1", args.NetworkAddress)
-	assert.Equal(t, int64(666), args.NetworkPort)
-	assert.Equal(t, 10, args.PrintGoroutinesEvery)
-	assert.Equal(t, "btrfs", args.StorageBackend)
-	assert.Equal(t, "/dev/sda2", args.StorageCreateDevice)
-	assert.Equal(t, int64(8192), args.StorageCreateLoop)
-	assert.Equal(t, "default", args.StorageDataset)
-	assert.Equal(t, true, args.Syslog)
-	assert.Equal(t, 30, args.Timeout)
-	assert.Equal(t, "sekret", args.TrustPassword)
-	assert.Equal(t, true, args.Verbose)
-	assert.Equal(t, true, args.Version)
-	assert.Equal(t, true, args.Force)
-}
diff --git a/lxd/main_callhook.go b/lxd/main_callhook.go
index cc0b5c858..c02a3e747 100644
--- a/lxd/main_callhook.go
+++ b/lxd/main_callhook.go
@@ -5,22 +5,52 @@ import (
 	"os"
 	"time"
 
+	"github.com/spf13/cobra"
+
 	"github.com/lxc/lxd/client"
 )
 
-func cmdCallHook(args *Args) error {
-	// Parse the arguments
-	if len(args.Params) < 3 {
-		return fmt.Errorf("Invalid arguments")
+type cmdCallhook struct {
+	cmd    *cobra.Command
+	global *cmdGlobal
+}
+
+func (c *cmdCallhook) Command() *cobra.Command {
+	cmd := &cobra.Command{}
+	cmd.Use = "callhook <path> <id> <state>"
+	cmd.Short = "Call container lifecycle hook in LXD"
+	cmd.Long = `Description:
+  Call container lifecycle hook in LXD
+
+  This internal command notifies LXD about a container lifecycle event
+  (start, stop, restart) and blocks until LXD has processed it.
+
+`
+	cmd.RunE = c.Run
+	cmd.Hidden = true
+
+	c.cmd = cmd
+	return cmd
+}
+
+func (c *cmdCallhook) Run(cmd *cobra.Command, args []string) error {
+	if len(args) < 2 {
+		cmd.Help()
+
+		if len(args) == 0 {
+			return nil
+		}
+
+		return fmt.Errorf("Missing required arguments")
 	}
 
-	path := args.Params[0]
-	id := args.Params[1]
-	state := args.Params[2]
+	path := args[0]
+	id := args[1]
+	state := args[2]
 	target := ""
 
 	// Connect to LXD
-	c, err := lxd.ConnectLXDUnix(fmt.Sprintf("%s/unix.socket", path), nil)
+	d, err := lxd.ConnectLXDUnix(fmt.Sprintf("%s/unix.socket", path), nil)
 	if err != nil {
 		return err
 	}
@@ -38,7 +68,7 @@ func cmdCallHook(args *Args) error {
 	// Setup the request
 	hook := make(chan error, 1)
 	go func() {
-		_, _, err := c.RawQuery("GET", url, nil, "")
+		_, _, err := d.RawQuery("GET", url, nil, "")
 		if err != nil {
 			hook <- err
 			return
diff --git a/lxd/main_daemon.go b/lxd/main_daemon.go
index 0703a6c32..5a8589ef5 100644
--- a/lxd/main_daemon.go
+++ b/lxd/main_daemon.go
@@ -7,12 +7,50 @@ import (
 	"os/signal"
 	"syscall"
 
+	"github.com/spf13/cobra"
+
 	dbg "github.com/lxc/lxd/lxd/debug"
 	"github.com/lxc/lxd/lxd/sys"
 	"github.com/lxc/lxd/shared/logger"
 )
 
-func cmdDaemon(args *Args) error {
+type cmdDaemon struct {
+	cmd    *cobra.Command
+	global *cmdGlobal
+
+	// Common options
+	flagGroup string
+
+	// Debug options
+	flagCPUProfile      string
+	flagMemoryProfile   string
+	flagPrintGoroutines int
+}
+
+func (c *cmdDaemon) Command() *cobra.Command {
+	cmd := &cobra.Command{}
+	cmd.Use = "lxd"
+	cmd.Short = "The LXD container manager (daemon)"
+	cmd.Long = `Description:
+  The LXD container manager (daemon)
+
+  This is the LXD daemon command line. It's typically started directly by your
+  init system and interacted with through a tool like ` + "`lxc`" + `.
+
+  There are however a number of subcommands that let you interact directly with
+  the local LXD daemon and which may not be performed through the REST API alone.
+`
+	cmd.RunE = c.Run
+	cmd.Flags().StringVar(&c.flagGroup, "group", "", "The group of users that will be allowed to talk to LXD"+"``")
+	cmd.Flags().StringVar(&c.flagCPUProfile, "cpu-profile", "", "Enable CPU profiling, writing into the specified file"+"``")
+	cmd.Flags().StringVar(&c.flagMemoryProfile, "memory-profile", "", "Enable memory profiling, writing into the specified file"+"``")
+	cmd.Flags().IntVar(&c.flagPrintGoroutines, "print-goroutines", 0, "How often to print all the goroutines"+"``")
+
+	c.cmd = cmd
+	return cmd
+}
+
+func (c *cmdDaemon) Run(cmd *cobra.Command, args []string) error {
 	// Only root should run this
 	if os.Geteuid() != 0 {
 		return fmt.Errorf("This must be run as root")
@@ -20,9 +58,9 @@ func cmdDaemon(args *Args) error {
 
 	// Start debug activities as per command line flags, if any.
 	stop, err := dbg.Start(
-		dbg.CPU(args.CPUProfile),
-		dbg.Memory(args.MemProfile),
-		dbg.Goroutines(args.PrintGoroutinesEvery),
+		dbg.CPU(c.flagCPUProfile),
+		dbg.Memory(c.flagMemoryProfile),
+		dbg.Goroutines(c.flagPrintGoroutines),
 	)
 	if err != nil {
 		return err
@@ -38,10 +76,11 @@ func cmdDaemon(args *Args) error {
 		}
 	}
 
-	c := DefaultDaemonConfig()
-	c.Group = args.Group
-	c.Trace = args.Trace
-	d := NewDaemon(c, sys.DefaultOS())
+	conf := DefaultDaemonConfig()
+	conf.Group = c.flagGroup
+	conf.Trace = c.global.flagLogTrace
+	d := NewDaemon(conf, sys.DefaultOS())
+
 	err = d.Init()
 	if err != nil {
 		return err
diff --git a/lxd/main_import.go b/lxd/main_import.go
index 842de6491..1abfd94ab 100644
--- a/lxd/main_import.go
+++ b/lxd/main_import.go
@@ -3,25 +3,63 @@ package main
 import (
 	"fmt"
 
+	"github.com/spf13/cobra"
+
 	"github.com/lxc/lxd/client"
 )
 
-func cmdImport(args *Args) error {
-	if len(args.Params) < 1 {
-		return fmt.Errorf("please specify a container to import")
+type cmdImport struct {
+	cmd    *cobra.Command
+	global *cmdGlobal
+
+	flagForce bool
+}
+
+func (c *cmdImport) Command() *cobra.Command {
+	cmd := &cobra.Command{}
+	cmd.Use = "import <container name>"
+	cmd.Short = "Import existing containers"
+	cmd.Long = `Description:
+  Import existing containers
+
+  This command is mostly used for disaster recovery. It lets you attempt
+  to recreate all database entries for containers that LXD no longer knows
+  about.
+
+  To do so, you must first mount your container storage at the expected
+  path inside the storage-pools directory. Once that's in place,
+  ` + "`lxd import`" + ` can be called for each individual container.
+`
+	cmd.RunE = c.Run
+	cmd.Flags().BoolVarP(&c.flagForce, "force", "f", false, "Force the import (override existing data or partial restore)")
+
+	c.cmd = cmd
+	return cmd
+}
+
+func (c *cmdImport) Run(cmd *cobra.Command, args []string) error {
+	if len(args) < 1 {
+		cmd.Help()
+
+		if len(args) == 0 {
+			return nil
+		}
+
+		return fmt.Errorf("Missing required arguments")
 	}
-	name := args.Params[0]
+
+	name := args[0]
 	req := map[string]interface{}{
 		"name":  name,
-		"force": args.Force,
+		"force": c.flagForce,
 	}
 
-	c, err := lxd.ConnectLXDUnix("", nil)
+	d, err := lxd.ConnectLXDUnix("", nil)
 	if err != nil {
 		return err
 	}
 
-	_, _, err = c.RawQuery("POST", "/internal/containers", req, "")
+	_, _, err = d.RawQuery("POST", "/internal/containers", req, "")
 	if err != nil {
 		return err
 	}
diff --git a/lxd/main_migratedumpsuccess.go b/lxd/main_migratedumpsuccess.go
index 85603534b..683a46a9f 100644
--- a/lxd/main_migratedumpsuccess.go
+++ b/lxd/main_migratedumpsuccess.go
@@ -4,28 +4,58 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/spf13/cobra"
+
 	"github.com/lxc/lxd/client"
 	"github.com/lxc/lxd/shared/api"
 )
 
-func cmdMigrateDumpSuccess(args *Args) error {
-	if len(args.Params) != 2 {
-		return fmt.Errorf("bad migrate dump success args %s", args.Params)
+type cmdMigratedumpsuccess struct {
+	cmd    *cobra.Command
+	global *cmdGlobal
+}
+
+func (c *cmdMigratedumpsuccess) Command() *cobra.Command {
+	cmd := &cobra.Command{}
+	cmd.Use = "migratedumpsuccess <operation> <secret>"
+	cmd.Short = "Tell LXD that a particular CRIU dump succeeded"
+	cmd.Long = `Description:
+  Tell LXD that a particular CRIU dump succeeded
+
+  This internal command is used to from the CRIU dump script and is
+  called as soon as the script is done running.
+`
+	cmd.RunE = c.Run
+	cmd.Hidden = true
+
+	c.cmd = cmd
+	return cmd
+}
+
+func (c *cmdMigratedumpsuccess) Run(cmd *cobra.Command, args []string) error {
+	if len(args) < 2 {
+		cmd.Help()
+
+		if len(args) == 0 {
+			return nil
+		}
+
+		return fmt.Errorf("Missing required arguments")
 	}
 
-	c, err := lxd.ConnectLXDUnix("", nil)
+	d, err := lxd.ConnectLXDUnix("", nil)
 	if err != nil {
 		return err
 	}
 
-	url := fmt.Sprintf("%s/websocket?secret=%s", strings.TrimPrefix(args.Params[0], "/1.0"), args.Params[1])
-	conn, err := c.RawWebsocket(url)
+	url := fmt.Sprintf("%s/websocket?secret=%s", strings.TrimPrefix(args[0], "/1.0"), args[1])
+	conn, err := d.RawWebsocket(url)
 	if err != nil {
 		return err
 	}
 	conn.Close()
 
-	resp, _, err := c.RawQuery("GET", fmt.Sprintf("%s/wait", args.Params[0]), nil, "")
+	resp, _, err := d.RawQuery("GET", fmt.Sprintf("%s/wait", args[0]), nil, "")
 	if err != nil {
 		return err
 	}
diff --git a/lxd/main_netcat.go b/lxd/main_netcat.go
index 26e2e8b0c..0d71f3d6e 100644
--- a/lxd/main_netcat.go
+++ b/lxd/main_netcat.go
@@ -7,23 +7,49 @@ import (
 	"os"
 	"sync"
 
+	"github.com/spf13/cobra"
+
 	"github.com/lxc/lxd/shared"
 	"github.com/lxc/lxd/shared/eagain"
 )
 
-// Netcat is called with:
-//
-//    lxd netcat /path/to/unix/socket
-//
-// and does unbuffered netcatting of to socket to stdin/stdout. Any arguments
-// after the path to the unix socket are ignored, so that this can be passed
-// directly to rsync as the sync command.
-func cmdNetcat(args *Args) error {
-	if len(args.Params) < 2 {
-		return fmt.Errorf("Bad arguments %q", args.Params)
+type cmdNetcat struct {
+	cmd    *cobra.Command
+	global *cmdGlobal
+}
+
+func (c *cmdNetcat) Command() *cobra.Command {
+	cmd := &cobra.Command{}
+	cmd.Use = "netcat <address> <name>"
+	cmd.Short = "Send stdin data to a unix socket"
+	cmd.Long = `Description:
+  Send stdin data to a unix socket
+
+  This internal command is used to forward the output of a program over
+  a websocket by first forwarding it to a unix socket controlled by LXD.
+
+  Its main use is when running rsync or btrfs/zfs send/receive between
+  two machines over the LXD websocket API.
+`
+	cmd.RunE = c.Run
+	cmd.Hidden = true
+
+	c.cmd = cmd
+	return cmd
+}
+
+func (c *cmdNetcat) Run(cmd *cobra.Command, args []string) error {
+	if len(args) < 2 {
+		cmd.Help()
+
+		if len(args) == 0 {
+			return nil
+		}
+
+		return fmt.Errorf("Missing required arguments")
 	}
 
-	logPath := shared.LogPath(args.Params[1], "netcat.log")
+	logPath := shared.LogPath(args[1], "netcat.log")
 	if shared.PathExists(logPath) {
 		os.Remove(logPath)
 	}
@@ -34,15 +60,15 @@ func cmdNetcat(args *Args) error {
 	}
 	defer logFile.Close()
 
-	uAddr, err := net.ResolveUnixAddr("unix", args.Params[0])
+	uAddr, err := net.ResolveUnixAddr("unix", args[0])
 	if err != nil {
-		logFile.WriteString(fmt.Sprintf("Could not resolve unix domain socket \"%s\": %s.\n", args.Params[0], err))
+		logFile.WriteString(fmt.Sprintf("Could not resolve unix domain socket \"%s\": %s.\n", args[0], err))
 		return err
 	}
 
 	conn, err := net.DialUnix("unix", nil, uAddr)
 	if err != nil {
-		logFile.WriteString(fmt.Sprintf("Could not dial unix domain socket \"%s\": %s.\n", args.Params[0], err))
+		logFile.WriteString(fmt.Sprintf("Could not dial unix domain socket \"%s\": %s.\n", args[0], err))
 		return err
 	}
 
@@ -52,7 +78,7 @@ func cmdNetcat(args *Args) error {
 	go func() {
 		_, err := io.Copy(eagain.Writer{Writer: os.Stdout}, eagain.Reader{Reader: conn})
 		if err != nil {
-			logFile.WriteString(fmt.Sprintf("Error while copying from stdout to unix domain socket \"%s\": %s.\n", args.Params[0], err))
+			logFile.WriteString(fmt.Sprintf("Error while copying from stdout to unix domain socket \"%s\": %s.\n", args[0], err))
 		}
 		conn.Close()
 		wg.Done()
@@ -61,7 +87,7 @@ func cmdNetcat(args *Args) error {
 	go func() {
 		_, err := io.Copy(eagain.Writer{Writer: conn}, eagain.Reader{Reader: os.Stdin})
 		if err != nil {
-			logFile.WriteString(fmt.Sprintf("Error while copying from unix domain socket \"%s\" to stdin: %s.\n", args.Params[0], err))
+			logFile.WriteString(fmt.Sprintf("Error while copying from unix domain socket \"%s\" to stdin: %s.\n", args[0], err))
 		}
 	}()
 
diff --git a/lxd/main_shutdown.go b/lxd/main_shutdown.go
index 8e6d642be..68c5d7956 100644
--- a/lxd/main_shutdown.go
+++ b/lxd/main_shutdown.go
@@ -5,19 +5,49 @@ import (
 	"strings"
 	"time"
 
+	"github.com/spf13/cobra"
+
 	"github.com/lxc/lxd/client"
 )
 
-func cmdShutdown(args *Args) error {
+type cmdShutdown struct {
+	cmd    *cobra.Command
+	global *cmdGlobal
+
+	flagTimeout int
+}
+
+func (c *cmdShutdown) Command() *cobra.Command {
+	cmd := &cobra.Command{}
+	cmd.Use = "shutdown"
+	cmd.Short = "Tell LXD to shutdown all containers and exit"
+	cmd.Long = `Description:
+  Tell LXD to shutdown all containers and exit
+
+  This will tell LXD to start a clean shutdown of all containers,
+  followed by having itself shutdown and exit.
+
+  This can take quite a while as containers can take a long time to
+  shutdown, especially if a non-standard timeout was configured for them.
+`
+	cmd.RunE = c.Run
+	cmd.Flags().IntVarP(&c.flagTimeout, "timeout", "t", 0, "Number of seconds to wait before giving up"+"``")
+
+	c.cmd = cmd
+	return cmd
+}
+
+func (c *cmdShutdown) Run(cmd *cobra.Command, args []string) error {
 	connArgs := &lxd.ConnectionArgs{
 		SkipGetServer: true,
 	}
-	c, err := lxd.ConnectLXDUnix("", connArgs)
+
+	d, err := lxd.ConnectLXDUnix("", connArgs)
 	if err != nil {
 		return err
 	}
 
-	_, _, err = c.RawQuery("PUT", "/internal/shutdown", nil, "")
+	_, _, err = d.RawQuery("PUT", "/internal/shutdown", nil, "")
 	if err != nil && !strings.HasSuffix(err.Error(), ": EOF") {
 		// NOTE: if we got an EOF error here it means that the daemon
 		// has shutdown so quickly that it already closed the unix
@@ -27,7 +57,7 @@ func cmdShutdown(args *Args) error {
 
 	chMonitor := make(chan bool, 1)
 	go func() {
-		monitor, err := c.GetEvents()
+		monitor, err := d.GetEvents()
 		if err != nil {
 			close(chMonitor)
 			return
@@ -37,12 +67,12 @@ func cmdShutdown(args *Args) error {
 		close(chMonitor)
 	}()
 
-	if args.Timeout > 0 {
+	if c.flagTimeout > 0 {
 		select {
 		case <-chMonitor:
 			break
-		case <-time.After(time.Second * time.Duration(args.Timeout)):
-			return fmt.Errorf("LXD still running after %ds timeout.", args.Timeout)
+		case <-time.After(time.Second * time.Duration(c.flagTimeout)):
+			return fmt.Errorf("LXD still running after %ds timeout", c.flagTimeout)
 		}
 	} else {
 		<-chMonitor
diff --git a/lxd/main_sql.go b/lxd/main_sql.go
index f22f67375..b60863ff4 100644
--- a/lxd/main_sql.go
+++ b/lxd/main_sql.go
@@ -7,17 +7,52 @@ import (
 	"strings"
 	"time"
 
-	lxd "github.com/lxc/lxd/client"
+	"github.com/spf13/cobra"
+
+	"github.com/lxc/lxd/client"
 )
 
-func cmdSQL(args *Args) error {
-	if len(args.Params) != 1 {
-		return fmt.Errorf("Invalid arguments")
+type cmdSql struct {
+	cmd    *cobra.Command
+	global *cmdGlobal
+}
+
+func (c *cmdSql) Command() *cobra.Command {
+	cmd := &cobra.Command{}
+	cmd.Use = "sql <query>"
+	cmd.Short = "Execute a SQL query against the LXD database"
+	cmd.Long = `Description:
+  Execute a SQL query against the LXD database
+
+  This internal command is mostly useful for debugging and disaster
+  recovery. The LXD team will occasionally provide hotfixes to users as a
+  set of database queries to fix some data inconsistency.
+
+  This command targets the global LXD database and works in both local
+  and cluster mode.
+`
+	cmd.RunE = c.Run
+	cmd.Hidden = true
+
+	c.cmd = cmd
+	return cmd
+}
+
+func (c *cmdSql) Run(cmd *cobra.Command, args []string) error {
+	if len(args) != 1 {
+		cmd.Help()
+
+		if len(args) == 0 {
+			return nil
+		}
+
+		return fmt.Errorf("Missing required arguments")
 	}
-	query := args.Params[0]
+
+	query := args[0]
 
 	// Connect to LXD
-	c, err := lxd.ConnectLXDUnix("", nil)
+	d, err := lxd.ConnectLXDUnix("", nil)
 	if err != nil {
 		return err
 	}
@@ -25,7 +60,7 @@ func cmdSQL(args *Args) error {
 	data := internalSQLPost{
 		Query: query,
 	}
-	response, _, err := c.RawQuery("POST", "/internal/sql", data, "")
+	response, _, err := d.RawQuery("POST", "/internal/sql", data, "")
 	if err != nil {
 		return err
 	}
diff --git a/lxd/main_waitready.go b/lxd/main_waitready.go
index 3edca01cd..6ddbf7406 100644
--- a/lxd/main_waitready.go
+++ b/lxd/main_waitready.go
@@ -4,28 +4,47 @@ import (
 	"fmt"
 	"time"
 
+	"github.com/spf13/cobra"
+
 	"github.com/lxc/lxd/client"
 )
 
-func cmdWaitReady(args *Args) error {
-	var timeout int
+type cmdWaitready struct {
+	cmd    *cobra.Command
+	global *cmdGlobal
 
-	if args.Timeout == -1 {
-		timeout = 15
-	} else {
-		timeout = args.Timeout
-	}
+	flagTimeout int
+}
+
+func (c *cmdWaitready) Command() *cobra.Command {
+	cmd := &cobra.Command{}
+	cmd.Use = "waitready"
+	cmd.Short = "Wait for LXD to be ready to process requests"
+	cmd.Long = `Description:
+  Wait for LXD to be ready to process requests
 
+  This command will block until LXD is reachable over its REST API and
+  it's done with early start tasks like re-starting previously started
+  containers.
+`
+	cmd.RunE = c.Run
+	cmd.Flags().IntVarP(&c.flagTimeout, "timeout", "t", 0, "Number of seconds to wait before giving up"+"``")
+
+	c.cmd = cmd
+	return cmd
+}
+
+func (c *cmdWaitready) Run(cmd *cobra.Command, args []string) error {
 	finger := make(chan error, 1)
 	go func() {
 		for {
-			c, err := lxd.ConnectLXDUnix("", nil)
+			d, err := lxd.ConnectLXDUnix("", nil)
 			if err != nil {
 				time.Sleep(500 * time.Millisecond)
 				continue
 			}
 
-			_, _, err = c.RawQuery("GET", "/internal/ready", nil, "")
+			_, _, err = d.RawQuery("GET", "/internal/ready", nil, "")
 			if err != nil {
 				time.Sleep(500 * time.Millisecond)
 				continue
@@ -36,11 +55,15 @@ func cmdWaitReady(args *Args) error {
 		}
 	}()
 
-	select {
-	case <-finger:
-		break
-	case <-time.After(time.Second * time.Duration(timeout)):
-		return fmt.Errorf("LXD still not running after %ds timeout.", timeout)
+	if c.flagTimeout > 0 {
+		select {
+		case <-finger:
+			break
+		case <-time.After(time.Second * time.Duration(c.flagTimeout)):
+			return fmt.Errorf("LXD still not running after %ds timeout", c.flagTimeout)
+		}
+	} else {
+		<-finger
 	}
 
 	return nil
diff --git a/test/suites/profiling.sh b/test/suites/profiling.sh
index 63859bd95..006d1badc 100644
--- a/test/suites/profiling.sh
+++ b/test/suites/profiling.sh
@@ -1,7 +1,7 @@
 test_cpu_profiling() {
   LXD3_DIR=$(mktemp -d -p "${TEST_DIR}" XXX)
   chmod +x "${LXD3_DIR}"
-  spawn_lxd "${LXD3_DIR}" false --cpuprofile "${LXD3_DIR}/cpu.out"
+  spawn_lxd "${LXD3_DIR}" false --cpu-profile "${LXD3_DIR}/cpu.out"
   lxdpid=$(cat "${LXD3_DIR}/lxd.pid")
   kill -TERM "${lxdpid}"
   wait "${lxdpid}"
@@ -20,7 +20,7 @@ test_cpu_profiling() {
 test_mem_profiling() {
   LXD4_DIR=$(mktemp -d -p "${TEST_DIR}" XXX)
   chmod +x "${LXD4_DIR}"
-  spawn_lxd "${LXD4_DIR}" false --memprofile "${LXD4_DIR}/mem"
+  spawn_lxd "${LXD4_DIR}" false --memory-profile "${LXD4_DIR}/mem"
   lxdpid=$(cat "${LXD4_DIR}/lxd.pid")
 
   if [ -e "${LXD4_DIR}/mem" ]; then

From 031577b089f27bb80ce203e44dc0dd075b4f5cfa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Sat, 10 Mar 2018 08:01:00 +0100
Subject: [PATCH 05/19] lxd: Port forkfile to cobra
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/container_lxc.go |  14 +-
 lxd/main.go          |   4 +
 lxd/main_forkfile.go | 518 +++++++++++++++++++++++++++++++++++++++++++++++++++
 lxd/main_nsexec.go   | 459 +++------------------------------------------
 4 files changed, 557 insertions(+), 438 deletions(-)
 create mode 100644 lxd/main_forkfile.go

diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index b73b0c9e5..d1eb1b109 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -5191,7 +5191,8 @@ func (c *containerLXC) FileExists(path string) error {
 	// Check if the file exists in the container
 	out, err := shared.RunCommand(
 		c.state.OS.ExecPath,
-		"forkcheckfile",
+		"forkfile",
+		"exists",
 		c.RootfsPath(),
 		fmt.Sprintf("%d", c.InitPID()),
 		path,
@@ -5237,11 +5238,12 @@ func (c *containerLXC) FilePull(srcpath string, dstpath string) (int64, int64, o
 	// Get the file from the container
 	out, err := shared.RunCommand(
 		c.state.OS.ExecPath,
-		"forkgetfile",
+		"forkfile",
+		"pull",
 		c.RootfsPath(),
 		fmt.Sprintf("%d", c.InitPID()),
-		dstpath,
 		srcpath,
+		dstpath,
 	)
 
 	// Tear down container storage if needed
@@ -5380,7 +5382,8 @@ func (c *containerLXC) FilePush(type_ string, srcpath string, dstpath string, ui
 	// Push the file to the container
 	out, err := shared.RunCommand(
 		c.state.OS.ExecPath,
-		"forkputfile",
+		"forkfile",
+		"push",
 		c.RootfsPath(),
 		fmt.Sprintf("%d", c.InitPID()),
 		srcpath,
@@ -5448,7 +5451,8 @@ func (c *containerLXC) FileRemove(path string) error {
 	// Remove the file from the container
 	out, err := shared.RunCommand(
 		c.state.OS.ExecPath,
-		"forkremovefile",
+		"forkfile",
+		"remove",
 		c.RootfsPath(),
 		fmt.Sprintf("%d", c.InitPID()),
 		path,
diff --git a/lxd/main.go b/lxd/main.go
index 9673628e5..69898d119 100644
--- a/lxd/main.go
+++ b/lxd/main.go
@@ -84,6 +84,10 @@ func main() {
 	callhookCmd := cmdCallhook{global: &globalCmd}
 	app.AddCommand(callhookCmd.Command())
 
+	// forkfile sub-command
+	forkfileCmd := cmdForkfile{global: &globalCmd}
+	app.AddCommand(forkfileCmd.Command())
+
 	// import sub-command
 	importCmd := cmdImport{global: &globalCmd}
 	app.AddCommand(importCmd.Command())
diff --git a/lxd/main_forkfile.go b/lxd/main_forkfile.go
new file mode 100644
index 000000000..c1f3c9a97
--- /dev/null
+++ b/lxd/main_forkfile.go
@@ -0,0 +1,518 @@
+package main
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+)
+
+/*
+#define _GNU_SOURCE
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#define ADVANCE_ARG_REQUIRED() \
+	do { \
+		while (*cur != 0) \
+			cur++; \
+		cur++; \
+		if (size <= cur - buf) { \
+			fprintf(stderr, "not enough arguments\n"); \
+			_exit(1); \
+		} \
+	} while(0)
+
+extern void error(char *msg);
+extern void attach_userns(int pid);
+extern int dosetns(int pid, char *nstype);
+
+int copy(int target, int source, bool append)
+{
+	ssize_t n;
+	char buf[1024];
+
+	if (!append && ftruncate(target, 0) < 0) {
+		error("error: truncate");
+		return -1;
+	}
+
+	if (append && lseek(target, 0, SEEK_END) < 0) {
+		error("error: seek");
+		return -1;
+	}
+
+	while ((n = read(source, buf, 1024)) > 0) {
+		if (write(target, buf, n) != n) {
+			error("error: write");
+			return -1;
+		}
+	}
+
+	if (n < 0) {
+		error("error: read");
+		return -1;
+	}
+
+	return 0;
+}
+
+int manip_file_in_ns(char *rootfs, int pid, char *host, char *container, bool is_put, char *type, uid_t uid, gid_t gid, mode_t mode, uid_t defaultUid, gid_t defaultGid, mode_t defaultMode, bool append) {
+	int host_fd = -1, container_fd = -1;
+	int ret = -1;
+	int container_open_flags;
+	struct stat st;
+	int exists = 1;
+	bool is_dir_manip = type != NULL && !strcmp(type, "directory");
+	bool is_symlink_manip = type != NULL && !strcmp(type, "symlink");
+	char link_target[PATH_MAX];
+	ssize_t link_length;
+
+	if (!is_dir_manip && !is_symlink_manip) {
+		host_fd = open(host, O_RDWR);
+		if (host_fd < 0) {
+			error("error: open");
+			return -1;
+		}
+	}
+
+	if (pid > 0) {
+		attach_userns(pid);
+
+		if (dosetns(pid, "mnt") < 0) {
+			error("error: setns");
+			goto close_host;
+		}
+	} else {
+		if (chroot(rootfs) < 0) {
+			error("error: chroot");
+			goto close_host;
+		}
+
+		if (chdir("/") < 0) {
+			error("error: chdir");
+			goto close_host;
+		}
+	}
+
+	if (is_put && is_dir_manip) {
+		if (mode == -1) {
+			mode = defaultMode;
+		}
+
+		if (uid == -1) {
+			uid = defaultUid;
+		}
+
+		if (gid == -1) {
+			gid = defaultGid;
+		}
+
+		if (mkdir(container, mode) < 0 && errno != EEXIST) {
+			error("error: mkdir");
+			return -1;
+		}
+
+		if (chown(container, uid, gid) < 0) {
+			error("error: chown");
+			return -1;
+		}
+
+		return 0;
+	}
+
+	if (is_put && is_symlink_manip) {
+		if (mode == -1) {
+			mode = defaultMode;
+		}
+
+		if (uid == -1) {
+			uid = defaultUid;
+		}
+
+		if (gid == -1) {
+			gid = defaultGid;
+		}
+
+		if (symlink(host, container) < 0 && errno != EEXIST) {
+			error("error: symlink");
+			return -1;
+		}
+
+		if (fchownat(0, container, uid, gid, AT_SYMLINK_NOFOLLOW) < 0) {
+			error("error: chown");
+			return -1;
+		}
+
+		return 0;
+	}
+
+	if (fstatat(AT_FDCWD, container, &st, AT_SYMLINK_NOFOLLOW) < 0)
+		exists = 0;
+
+	container_open_flags = O_RDWR;
+	if (is_put)
+		container_open_flags |= O_CREAT;
+
+	if (is_put && !is_dir_manip && exists && S_ISDIR(st.st_mode)) {
+		error("error: Path already exists as a directory");
+		goto close_host;
+	}
+
+	if (exists && S_ISDIR(st.st_mode))
+		container_open_flags = O_DIRECTORY;
+
+	if (!is_put && exists && S_ISLNK(st.st_mode)) {
+		fprintf(stderr, "uid: %ld\n", (long)st.st_uid);
+		fprintf(stderr, "gid: %ld\n", (long)st.st_gid);
+		fprintf(stderr, "mode: %ld\n", (unsigned long)st.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO));
+		fprintf(stderr, "type: symlink\n");
+
+		link_length = readlink(container, link_target, PATH_MAX);
+		if (link_length < 0 || link_length >= PATH_MAX) {
+			error("error: readlink");
+			goto close_host;
+		}
+		link_target[link_length] = '\0';
+
+		dprintf(host_fd, "%s\n", link_target);
+		goto close_container;
+	}
+
+	umask(0);
+	container_fd = open(container, container_open_flags, 0);
+	if (container_fd < 0) {
+		error("error: open");
+		goto close_host;
+	}
+	if (is_put) {
+		if (!exists) {
+			if (mode == -1) {
+				mode = defaultMode;
+			}
+
+			if (uid == -1) {
+				uid = defaultUid;
+			}
+
+			if (gid == -1) {
+				gid = defaultGid;
+			}
+		}
+
+		if (copy(container_fd, host_fd, append) < 0) {
+			error("error: copy");
+			goto close_container;
+		}
+
+		if (mode != -1 && fchmod(container_fd, mode) < 0) {
+			error("error: chmod");
+			goto close_container;
+		}
+
+		if (fchown(container_fd, uid, gid) < 0) {
+			error("error: chown");
+			goto close_container;
+		}
+		ret = 0;
+	} else {
+		if (fstat(container_fd, &st) < 0) {
+			error("error: stat");
+			goto close_container;
+		}
+
+		fprintf(stderr, "uid: %ld\n", (long)st.st_uid);
+		fprintf(stderr, "gid: %ld\n", (long)st.st_gid);
+		fprintf(stderr, "mode: %ld\n", (unsigned long)st.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO));
+		if (S_ISDIR(st.st_mode)) {
+			DIR *fdir;
+			struct dirent *de;
+
+			fdir = fdopendir(container_fd);
+			if (!fdir) {
+				error("error: fdopendir");
+				goto close_container;
+			}
+
+			fprintf(stderr, "type: directory\n");
+
+			while((de = readdir(fdir))) {
+				int len, i;
+
+				if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, ".."))
+					continue;
+
+				fprintf(stderr, "entry: ");
+
+				// swap \n to \0 since we split this output by line
+				for (i = 0, len = strlen(de->d_name); i < len; i++) {
+					if (*(de->d_name + i) == '\n')
+						putc(0, stderr);
+					else
+						putc(*(de->d_name + i), stderr);
+				}
+				fprintf(stderr, "\n");
+			}
+
+			closedir(fdir);
+			// container_fd is dead now that we fdopendir'd it
+			goto close_host;
+		} else {
+			fprintf(stderr, "type: file\n");
+			ret = copy(host_fd, container_fd, false);
+		}
+		fprintf(stderr, "type: %s", S_ISDIR(st.st_mode) ? "directory" : "file");
+	}
+
+close_container:
+	close(container_fd);
+close_host:
+	close(host_fd);
+	return ret;
+}
+
+void forkdofile(char *buf, char *cur, ssize_t size, bool is_put, char *rootfs, pid_t pid) {
+	uid_t uid = 0;
+	uid_t defaultUid = 0;
+
+	gid_t gid = 0;
+	gid_t defaultGid = 0;
+
+	mode_t mode = 0;
+	mode_t defaultMode = 0;
+
+	char *source = NULL;
+	char *target = NULL;
+	char *writeMode = NULL;
+	char *type = NULL;
+
+	bool append = false;
+
+
+	ADVANCE_ARG_REQUIRED();
+	if (is_put) {
+		source = cur;
+	} else {
+		target = cur;
+	}
+
+	ADVANCE_ARG_REQUIRED();
+	if (is_put) {
+		target = cur;
+	} else {
+		source = cur;
+	}
+
+	if (is_put) {
+		ADVANCE_ARG_REQUIRED();
+		type = cur;
+
+		ADVANCE_ARG_REQUIRED();
+		uid = atoi(cur);
+
+		ADVANCE_ARG_REQUIRED();
+		gid = atoi(cur);
+
+		ADVANCE_ARG_REQUIRED();
+		mode = atoi(cur);
+
+		ADVANCE_ARG_REQUIRED();
+		defaultUid = atoi(cur);
+
+		ADVANCE_ARG_REQUIRED();
+		defaultGid = atoi(cur);
+
+		ADVANCE_ARG_REQUIRED();
+		defaultMode = atoi(cur);
+
+		ADVANCE_ARG_REQUIRED();
+		if (strcmp(cur, "append") == 0) {
+			append = true;
+		}
+	}
+
+	printf("%d: %s to %s\n", is_put, source, target);
+
+	_exit(manip_file_in_ns(rootfs, pid, source, target, is_put, type, uid, gid, mode, defaultUid, defaultGid, defaultMode, append));
+}
+
+void forkcheckfile(char *buf, char *cur, ssize_t size, char *rootfs, pid_t pid) {
+	char *path = NULL;
+
+	ADVANCE_ARG_REQUIRED();
+	path = cur;
+
+	if (pid > 0) {
+		attach_userns(pid);
+
+		if (dosetns(pid, "mnt") < 0) {
+			error("error: setns");
+			_exit(1);
+		}
+	} else {
+		if (chroot(rootfs) < 0) {
+			error("error: chroot");
+			_exit(1);
+		}
+
+		if (chdir("/") < 0) {
+			error("error: chdir");
+			_exit(1);
+		}
+	}
+
+	if (access(path, F_OK) < 0) {
+		fprintf(stderr, "Path doesn't exist: %s\n", strerror(errno));
+		_exit(1);
+	}
+
+	_exit(0);
+}
+
+void forkremovefile(char *buf, char *cur, ssize_t size, char *rootfs, pid_t pid) {
+	char *path = NULL;
+	struct stat sb;
+
+	ADVANCE_ARG_REQUIRED();
+	path = cur;
+
+	if (pid > 0) {
+		attach_userns(pid);
+
+		if (dosetns(pid, "mnt") < 0) {
+			error("error: setns");
+			_exit(1);
+		}
+	} else {
+		if (chroot(rootfs) < 0) {
+			error("error: chroot");
+			_exit(1);
+		}
+
+		if (chdir("/") < 0) {
+			error("error: chdir");
+			_exit(1);
+		}
+	}
+
+	if (stat(path, &sb) < 0) {
+		error("error: stat");
+		_exit(1);
+	}
+
+	if ((sb.st_mode & S_IFMT) == S_IFDIR) {
+		if (rmdir(path) < 0) {
+			fprintf(stderr, "Failed to remove %s: %s\n", path, strerror(errno));
+			_exit(1);
+		}
+	} else {
+		if (unlink(path) < 0) {
+			fprintf(stderr, "Failed to remove %s: %s\n", path, strerror(errno));
+			_exit(1);
+		}
+	}
+
+	_exit(0);
+}
+
+void forkfile(char *buf, char *cur, ssize_t size) {
+	char *command = NULL;
+	char *rootfs = NULL;
+	pid_t pid = 0;
+
+	// Get the subcommand
+	ADVANCE_ARG_REQUIRED();
+	command = cur;
+
+	// Get the container rootfs
+	ADVANCE_ARG_REQUIRED();
+	if (strcmp(cur, "--help") == 0 || strcmp(cur, "--version") == 0 || strcmp(cur, "-h") == 0) {
+		return;
+	}
+
+	rootfs = cur;
+
+	// Get the container PID
+	ADVANCE_ARG_REQUIRED();
+	pid = atoi(cur);
+
+	// Check that we're root
+	if (geteuid() != 0) {
+		fprintf(stderr, "Error: forkfile requires root privileges\n");
+		_exit(1);
+	}
+
+	// Call the subcommands
+	if (strcmp(command, "push") == 0) {
+		forkdofile(buf, cur, size, true, rootfs, pid);
+	} else if (strcmp(command, "pull") == 0) {
+		forkdofile(buf, cur, size, false, rootfs, pid);
+	} else if (strcmp(command, "exists") == 0) {
+		forkcheckfile(buf, cur, size, rootfs, pid);
+	} else if (strcmp(command, "remove") == 0) {
+		forkremovefile(buf, cur, size, rootfs, pid);
+	}
+}
+*/
+import "C"
+
+type cmdForkfile struct {
+	cmd    *cobra.Command
+	global *cmdGlobal
+}
+
+func (c *cmdForkfile) Command() *cobra.Command {
+	// Main subcommand
+	cmd := &cobra.Command{}
+	cmd.Use = "forkfile"
+	cmd.Short = "Perform container file operations"
+	cmd.Long = `Description:
+  Perform container file operations
+
+  This set of internal commands are used for all container file
+  operations, attaching to the container's root filesystem and mount
+  namespace.
+`
+	cmd.Hidden = true
+
+	// pull
+	cmdPull := &cobra.Command{}
+	cmdPull.Use = "pull <rootfs> <PID> <source> <destination>"
+	cmdPull.Args = cobra.ExactArgs(4)
+	cmdPull.RunE = c.Run
+	cmd.AddCommand(cmdPull)
+
+	// push
+	cmdPush := &cobra.Command{}
+	cmdPush.Use = "push <rootfs> <PID> <source> <destination> <type> <uid> <gid> <mode> <root uid> <root gid> <default mode> <write type>"
+	cmdPush.Args = cobra.ExactArgs(12)
+	cmdPush.RunE = c.Run
+	cmd.AddCommand(cmdPush)
+
+	// exists
+	cmdExists := &cobra.Command{}
+	cmdExists.Use = "exists <rootfs> <PID> <path>"
+	cmdExists.Args = cobra.ExactArgs(3)
+	cmdExists.RunE = c.Run
+	cmd.AddCommand(cmdExists)
+
+	// remove
+	cmdRemove := &cobra.Command{}
+	cmdRemove.Use = "remove <rootfs> <PID> <path>"
+	cmdRemove.Args = cobra.ExactArgs(3)
+	cmdRemove.RunE = c.Run
+	cmd.AddCommand(cmdRemove)
+
+	c.cmd = cmd
+	return cmd
+}
+
+func (c *cmdForkfile) Run(cmd *cobra.Command, args []string) error {
+	return fmt.Errorf("This command should have been intercepted in cgo")
+}
diff --git a/lxd/main_nsexec.go b/lxd/main_nsexec.go
index d33e0a438..67f2508bb 100644
--- a/lxd/main_nsexec.go
+++ b/lxd/main_nsexec.go
@@ -20,36 +20,36 @@ package main
 
 /*
 #define _GNU_SOURCE
-#include <string.h>
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <grp.h>
+#include <libgen.h>
+#include <linux/limits.h>
+#include <sched.h>
 #include <stdio.h>
 #include <stdlib.h>
+#include <string.h>
 #include <sys/mount.h>
-#include <sched.h>
-#include <linux/sched.h>
-#include <linux/limits.h>
-#include <sys/mman.h>
 #include <sys/stat.h>
 #include <sys/types.h>
-#include <fcntl.h>
-#include <stdbool.h>
 #include <unistd.h>
-#include <errno.h>
-#include <alloca.h>
-#include <libgen.h>
-#include <ifaddrs.h>
-#include <dirent.h>
-#include <grp.h>
 
-// This expects:
-//  ./lxd forkputfile /source/path <pid> /target/path
-// or
-//  ./lxd forkgetfile /target/path <pid> /soruce/path <uid> <gid> <mode>
-// i.e. 8 arguments, each which have a max length of PATH_MAX.
-// Unfortunately, lseek() and fstat() both fail (EINVAL and 0 size) for
-// procfs. Also, we can't mmap, because procfs doesn't support that, either.
-//
 #define CMDLINE_SIZE (8 * PATH_MAX)
 
+#define ADVANCE_ARG_REQUIRED() \
+	do { \
+		while (*cur != 0) \
+			cur++; \
+		cur++; \
+		if (size <= cur - buf) { \
+			fprintf(stderr, "not enough arguments\n"); \
+			_exit(1); \
+		} \
+	} while(0)
+
+extern void forkfile(char *buf, char *cur, ssize_t size);
+
 void error(char *msg)
 {
 	int old_errno = errno;
@@ -87,36 +87,6 @@ int mkdir_p(const char *dir, mode_t mode)
 	return 0;
 }
 
-int copy(int target, int source, bool append)
-{
-	ssize_t n;
-	char buf[1024];
-
-	if (!append && ftruncate(target, 0) < 0) {
-		error("error: truncate");
-		return -1;
-	}
-
-	if (append && lseek(target, 0, SEEK_END) < 0) {
-		error("error: seek");
-		return -1;
-	}
-
-	while ((n = read(source, buf, 1024)) > 0) {
-		if (write(target, buf, n) != n) {
-			error("error: write");
-			return -1;
-		}
-	}
-
-	if (n < 0) {
-		error("error: read");
-		return -1;
-	}
-
-	return 0;
-}
-
 int dosetns(int pid, char *nstype) {
 	int mntns;
 	char buf[PATH_MAX];
@@ -180,232 +150,6 @@ void attach_userns(int pid) {
 	}
 }
 
-int manip_file_in_ns(char *rootfs, int pid, char *host, char *container, bool is_put, char *type, uid_t uid, gid_t gid, mode_t mode, uid_t defaultUid, gid_t defaultGid, mode_t defaultMode, bool append) {
-	int host_fd = -1, container_fd = -1;
-	int ret = -1;
-	int container_open_flags;
-	struct stat st;
-	int exists = 1;
-	bool is_dir_manip = type != NULL && !strcmp(type, "directory");
-	bool is_symlink_manip = type != NULL && !strcmp(type, "symlink");
-	char link_target[PATH_MAX];
-	ssize_t link_length;
-
-	if (!is_dir_manip && !is_symlink_manip) {
-		host_fd = open(host, O_RDWR);
-		if (host_fd < 0) {
-			error("error: open");
-			return -1;
-		}
-	}
-
-	if (pid > 0) {
-		attach_userns(pid);
-
-		if (dosetns(pid, "mnt") < 0) {
-			error("error: setns");
-			goto close_host;
-		}
-	} else {
-		if (chroot(rootfs) < 0) {
-			error("error: chroot");
-			goto close_host;
-		}
-
-		if (chdir("/") < 0) {
-			error("error: chdir");
-			goto close_host;
-		}
-	}
-
-	if (is_put && is_dir_manip) {
-		if (mode == -1) {
-			mode = defaultMode;
-		}
-
-		if (uid == -1) {
-			uid = defaultUid;
-		}
-
-		if (gid == -1) {
-			gid = defaultGid;
-		}
-
-		if (mkdir(container, mode) < 0 && errno != EEXIST) {
-			error("error: mkdir");
-			return -1;
-		}
-
-		if (chown(container, uid, gid) < 0) {
-			error("error: chown");
-			return -1;
-		}
-
-		return 0;
-	}
-
-	if (is_put && is_symlink_manip) {
-		if (mode == -1) {
-			mode = defaultMode;
-		}
-
-		if (uid == -1) {
-			uid = defaultUid;
-		}
-
-		if (gid == -1) {
-			gid = defaultGid;
-		}
-
-		if (symlink(host, container) < 0 && errno != EEXIST) {
-			error("error: symlink");
-			return -1;
-		}
-
-		if (fchownat(0, container, uid, gid, AT_SYMLINK_NOFOLLOW) < 0) {
-			error("error: chown");
-			return -1;
-		}
-
-		return 0;
-	}
-
-	if (fstatat(AT_FDCWD, container, &st, AT_SYMLINK_NOFOLLOW) < 0)
-		exists = 0;
-
-	container_open_flags = O_RDWR;
-	if (is_put)
-		container_open_flags |= O_CREAT;
-
-	if (is_put && !is_dir_manip && exists && S_ISDIR(st.st_mode)) {
-		error("error: Path already exists as a directory");
-		goto close_host;
-	}
-
-	if (exists && S_ISDIR(st.st_mode))
-		container_open_flags = O_DIRECTORY;
-
-	if (!is_put && exists && S_ISLNK(st.st_mode)) {
-		fprintf(stderr, "uid: %ld\n", (long)st.st_uid);
-		fprintf(stderr, "gid: %ld\n", (long)st.st_gid);
-		fprintf(stderr, "mode: %ld\n", (unsigned long)st.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO));
-		fprintf(stderr, "type: symlink\n");
-
-		link_length = readlink(container, link_target, PATH_MAX);
-		if (link_length < 0 || link_length >= PATH_MAX) {
-			error("error: readlink");
-			goto close_host;
-		}
-		link_target[link_length] = '\0';
-
-		dprintf(host_fd, "%s\n", link_target);
-		goto close_container;
-	}
-
-	umask(0);
-	container_fd = open(container, container_open_flags, 0);
-	if (container_fd < 0) {
-		error("error: open");
-		goto close_host;
-	}
-
-	if (is_put) {
-		if (!exists) {
-			if (mode == -1) {
-				mode = defaultMode;
-			}
-
-			if (uid == -1) {
-				uid = defaultUid;
-			}
-
-			if (gid == -1) {
-				gid = defaultGid;
-			}
-		}
-
-		if (copy(container_fd, host_fd, append) < 0) {
-			error("error: copy");
-			goto close_container;
-		}
-
-		if (mode != -1 && fchmod(container_fd, mode) < 0) {
-			error("error: chmod");
-			goto close_container;
-		}
-
-		if (fchown(container_fd, uid, gid) < 0) {
-			error("error: chown");
-			goto close_container;
-		}
-		ret = 0;
-	} else {
-		if (fstat(container_fd, &st) < 0) {
-			error("error: stat");
-			goto close_container;
-		}
-
-		fprintf(stderr, "uid: %ld\n", (long)st.st_uid);
-		fprintf(stderr, "gid: %ld\n", (long)st.st_gid);
-		fprintf(stderr, "mode: %ld\n", (unsigned long)st.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO));
-		if (S_ISDIR(st.st_mode)) {
-			DIR *fdir;
-			struct dirent *de;
-
-			fdir = fdopendir(container_fd);
-			if (!fdir) {
-				error("error: fdopendir");
-				goto close_container;
-			}
-
-			fprintf(stderr, "type: directory\n");
-
-			while((de = readdir(fdir))) {
-				int len, i;
-
-				if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, ".."))
-					continue;
-
-				fprintf(stderr, "entry: ");
-
-				// swap \n to \0 since we split this output by line
-				for (i = 0, len = strlen(de->d_name); i < len; i++) {
-					if (*(de->d_name + i) == '\n')
-						putc(0, stderr);
-					else
-						putc(*(de->d_name + i), stderr);
-				}
-				fprintf(stderr, "\n");
-			}
-
-			closedir(fdir);
-			// container_fd is dead now that we fdopendir'd it
-			goto close_host;
-		} else {
-			fprintf(stderr, "type: file\n");
-			ret = copy(host_fd, container_fd, false);
-		}
-		fprintf(stderr, "type: %s", S_ISDIR(st.st_mode) ? "directory" : "file");
-	}
-
-close_container:
-	close(container_fd);
-close_host:
-	close(host_fd);
-	return ret;
-}
-
-#define ADVANCE_ARG_REQUIRED()					\
-	do {							\
-		while (*cur != 0)				\
-			cur++;					\
-		cur++;						\
-		if (size <= cur - buf) {			\
-			fprintf(stderr, "not enough arguments\n");	\
-			_exit(1);				\
-		}						\
-	} while(0)
-
 void ensure_dir(char *dest) {
 	struct stat sb;
 	if (stat(dest, &sb) == 0) {
@@ -540,153 +284,6 @@ void forkumount(char *buf, char *cur, ssize_t size) {
 	_exit(0);
 }
 
-void forkdofile(char *buf, char *cur, bool is_put, ssize_t size) {
-	uid_t uid = 0;
-	gid_t gid = 0;
-	mode_t mode = 0;
-	uid_t defaultUid = 0;
-	gid_t defaultGid = 0;
-	mode_t defaultMode = 0;
-	char *command = cur, *rootfs = NULL, *source = NULL, *target = NULL, *writeMode = NULL, *type = NULL;
-	pid_t pid;
-	bool append = false;
-
-	ADVANCE_ARG_REQUIRED();
-	rootfs = cur;
-
-	ADVANCE_ARG_REQUIRED();
-	pid = atoi(cur);
-
-	ADVANCE_ARG_REQUIRED();
-	source = cur;
-
-	ADVANCE_ARG_REQUIRED();
-	target = cur;
-
-	if (is_put) {
-		ADVANCE_ARG_REQUIRED();
-		type = cur;
-
-		ADVANCE_ARG_REQUIRED();
-		uid = atoi(cur);
-
-		ADVANCE_ARG_REQUIRED();
-		gid = atoi(cur);
-
-		ADVANCE_ARG_REQUIRED();
-		mode = atoi(cur);
-
-		ADVANCE_ARG_REQUIRED();
-		defaultUid = atoi(cur);
-
-		ADVANCE_ARG_REQUIRED();
-		defaultGid = atoi(cur);
-
-		ADVANCE_ARG_REQUIRED();
-		defaultMode = atoi(cur);
-
-		ADVANCE_ARG_REQUIRED();
-		if (strcmp(cur, "append") == 0) {
-			append = true;
-		}
-	}
-
-	_exit(manip_file_in_ns(rootfs, pid, source, target, is_put, type, uid, gid, mode, defaultUid, defaultGid, defaultMode, append));
-}
-
-void forkcheckfile(char *buf, char *cur, bool is_put, ssize_t size) {
-	char *command = cur, *rootfs = NULL, *path = NULL;
-	pid_t pid;
-
-	ADVANCE_ARG_REQUIRED();
-	rootfs = cur;
-
-	ADVANCE_ARG_REQUIRED();
-	pid = atoi(cur);
-
-	ADVANCE_ARG_REQUIRED();
-	path = cur;
-
-	if (pid > 0) {
-		attach_userns(pid);
-
-		if (dosetns(pid, "mnt") < 0) {
-			error("error: setns");
-			_exit(1);
-		}
-	} else {
-		if (chroot(rootfs) < 0) {
-			error("error: chroot");
-			_exit(1);
-		}
-
-		if (chdir("/") < 0) {
-			error("error: chdir");
-			_exit(1);
-		}
-	}
-
-	if (access(path, F_OK) < 0) {
-		fprintf(stderr, "Path doesn't exist: %s\n", strerror(errno));
-		_exit(1);
-	}
-
-	_exit(0);
-}
-
-void forkremovefile(char *buf, char *cur, bool is_put, ssize_t size) {
-	char *command = cur, *rootfs = NULL, *path = NULL;
-	pid_t pid;
-	struct stat sb;
-
-	ADVANCE_ARG_REQUIRED();
-	rootfs = cur;
-
-	ADVANCE_ARG_REQUIRED();
-	pid = atoi(cur);
-
-	ADVANCE_ARG_REQUIRED();
-	path = cur;
-
-	if (pid > 0) {
-		attach_userns(pid);
-
-		if (dosetns(pid, "mnt") < 0) {
-			error("error: setns");
-			_exit(1);
-		}
-	} else {
-		if (chroot(rootfs) < 0) {
-			error("error: chroot");
-			_exit(1);
-		}
-
-		if (chdir("/") < 0) {
-			error("error: chdir");
-			_exit(1);
-		}
-	}
-
-	if (stat(path, &sb) < 0) {
-		error("error: stat");
-		_exit(1);
-	}
-
-	if ((sb.st_mode & S_IFMT) == S_IFDIR) {
-		if (rmdir(path) < 0) {
-			fprintf(stderr, "Failed to remove %s: %s\n", path, strerror(errno));
-			_exit(1);
-		}
-	} else {
-		if (unlink(path) < 0) {
-			fprintf(stderr, "Failed to remove %s: %s\n", path, strerror(errno));
-			_exit(1);
-		}
-	}
-
-	_exit(0);
-}
-
 void forkgetnet(char *buf, char *cur, ssize_t size) {
 	ADVANCE_ARG_REQUIRED();
 	int pid = atoi(cur);
@@ -795,6 +392,7 @@ __attribute__((constructor)) void init(void) {
 	ssize_t size;
 	char *cur;
 
+	// Extract arguments
 	cmdline = open("/proc/self/cmdline", O_RDONLY);
 	if (cmdline < 0) {
 		error("error: open");
@@ -809,22 +407,17 @@ __attribute__((constructor)) void init(void) {
 	}
 	close(cmdline);
 
+	// Skip the first argument (but don't fail on missing second argument)
 	cur = buf;
-	// skip argv[0]
 	while (*cur != 0)
 		cur++;
 	cur++;
 	if (size <= cur - buf)
 		return;
 
-	if (strcmp(cur, "forkputfile") == 0) {
-		forkdofile(buf, cur, true, size);
-	} else if (strcmp(cur, "forkgetfile") == 0) {
-		forkdofile(buf, cur, false, size);
-	} else if (strcmp(cur, "forkcheckfile") == 0) {
-		forkcheckfile(buf, cur, false, size);
-	} else if (strcmp(cur, "forkremovefile") == 0) {
-		forkremovefile(buf, cur, false, size);
+	// Intercepts some subcommands
+	if (strcmp(cur, "forkfile") == 0) {
+		forkfile(buf, cur, size);
 	} else if (strcmp(cur, "forkmount") == 0) {
 		forkmount(buf, cur, size);
 	} else if (strcmp(cur, "forkumount") == 0) {

From ede4437ac82871de8be87e97fd05b0241d1a0ab0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Sat, 10 Mar 2018 12:37:06 +0100
Subject: [PATCH 06/19] lxd: Port forknet to cobra
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/container_lxc.go                        |  3 +-
 lxd/main.go                                 |  4 ++
 lxd/{main_forkgetnet.go => main_forknet.go} | 94 ++++++++++++++++++++++++++++-
 lxd/main_nsexec.go                          | 17 +-----
 4 files changed, 102 insertions(+), 16 deletions(-)
 rename lxd/{main_forkgetnet.go => main_forknet.go} (59%)

diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index d1eb1b109..56355e7d1 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -5704,7 +5704,8 @@ func (c *containerLXC) networkState() map[string]api.ContainerStateNetwork {
 	// Get the network state from the container
 	out, err := shared.RunCommand(
 		c.state.OS.ExecPath,
-		"forkgetnet",
+		"forknet",
+		"info",
 		fmt.Sprintf("%d", pid))
 
 	// Process forkgetnet response
diff --git a/lxd/main.go b/lxd/main.go
index 69898d119..496a7e715 100644
--- a/lxd/main.go
+++ b/lxd/main.go
@@ -88,6 +88,10 @@ func main() {
 	forkfileCmd := cmdForkfile{global: &globalCmd}
 	app.AddCommand(forkfileCmd.Command())
 
+	// forknet sub-command
+	forknetCmd := cmdForknet{global: &globalCmd}
+	app.AddCommand(forknetCmd.Command())
+
 	// import sub-command
 	importCmd := cmdImport{global: &globalCmd}
 	app.AddCommand(importCmd.Command())
diff --git a/lxd/main_forkgetnet.go b/lxd/main_forknet.go
similarity index 59%
rename from lxd/main_forkgetnet.go
rename to lxd/main_forknet.go
index d541c0941..f019c63f6 100644
--- a/lxd/main_forkgetnet.go
+++ b/lxd/main_forknet.go
@@ -8,10 +8,102 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/spf13/cobra"
+
 	"github.com/lxc/lxd/shared/api"
 )
 
-func cmdForkGetNet(args *Args) error {
+/*
+#define _GNU_SOURCE
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#define ADVANCE_ARG_REQUIRED() \
+	do { \
+		while (*cur != 0) \
+			cur++; \
+		cur++; \
+		if (size <= cur - buf) { \
+			fprintf(stderr, "not enough arguments\n"); \
+			_exit(1); \
+		} \
+	} while(0)
+
+extern int dosetns(int pid, char *nstype);
+
+void forkdonetinfo(char *buf, char *cur, ssize_t size, pid_t pid) {
+	if (dosetns(pid, "net") < 0) {
+		fprintf(stderr, "Failed setns to container network namespace: %s\n", strerror(errno));
+		_exit(1);
+	}
+
+	// Jump back to Go for the rest
+}
+
+void forknet(char *buf, char *cur, ssize_t size) {
+	char *command = NULL;
+	pid_t pid = 0;
+
+	// Get the subcommand
+	ADVANCE_ARG_REQUIRED();
+	command = cur;
+
+	// Get the pid
+	ADVANCE_ARG_REQUIRED();
+	if (strcmp(cur, "--help") == 0 || strcmp(cur, "--version") == 0 || strcmp(cur, "-h") == 0) {
+		return;
+	}
+
+	pid = atoi(cur);
+
+	// Check that we're root
+	if (geteuid() != 0) {
+		fprintf(stderr, "Error: forkfile requires root privileges\n");
+		_exit(1);
+	}
+
+	// Call the subcommands
+	if (strcmp(command, "info") == 0) {
+		forkdonetinfo(buf, cur, size, pid);
+	}
+}
+*/
+import "C"
+
+type cmdForknet struct {
+	cmd    *cobra.Command
+	global *cmdGlobal
+}
+
+func (c *cmdForknet) Command() *cobra.Command {
+	// Main subcommand
+	cmd := &cobra.Command{}
+	cmd.Use = "forknet"
+	cmd.Short = "Perform container network operations"
+	cmd.Long = `Description:
+  Perform container network operations
+
+  This set of internal commands are used for some container network
+  operations which require attaching to the container's network namespace.
+`
+	cmd.Hidden = true
+
+	// pull
+	cmdInfo := &cobra.Command{}
+	cmdInfo.Use = "info <PID>"
+	cmdInfo.Args = cobra.ExactArgs(1)
+	cmdInfo.RunE = c.RunInfo
+	cmd.AddCommand(cmdInfo)
+
+	c.cmd = cmd
+	return cmd
+}
+
+func (c *cmdForknet) RunInfo(cmd *cobra.Command, args []string) error {
 	networks := map[string]api.ContainerStateNetwork{}
 
 	interfaces, err := net.Interfaces()
diff --git a/lxd/main_nsexec.go b/lxd/main_nsexec.go
index 67f2508bb..a94be06f2 100644
--- a/lxd/main_nsexec.go
+++ b/lxd/main_nsexec.go
@@ -49,6 +49,7 @@ package main
 	} while(0)
 
 extern void forkfile(char *buf, char *cur, ssize_t size);
+extern void forknet(char *buf, char *cur, ssize_t size);
 
 void error(char *msg)
 {
@@ -284,18 +285,6 @@ void forkumount(char *buf, char *cur, ssize_t size) {
 	_exit(0);
 }
 
-void forkgetnet(char *buf, char *cur, ssize_t size) {
-	ADVANCE_ARG_REQUIRED();
-	int pid = atoi(cur);
-
-	if (dosetns(pid, "net") < 0) {
-		fprintf(stderr, "Failed setns to container network namespace: %s\n", strerror(errno));
-		_exit(1);
-	}
-
-	// The rest happens in Go
-}
-
 void forkproxy(char *buf, char *cur, ssize_t size) {
 	int cmdline, listen_pid, connect_pid, fdnum, forked, childPid, ret;
 	char fdpath[80];
@@ -422,8 +411,8 @@ __attribute__((constructor)) void init(void) {
 		forkmount(buf, cur, size);
 	} else if (strcmp(cur, "forkumount") == 0) {
 		forkumount(buf, cur, size);
-	} else if (strcmp(cur, "forkgetnet") == 0) {
-		forkgetnet(buf, cur, size);
+	} else if (strcmp(cur, "forknet") == 0) {
+		forknet(buf, cur, size);
 	} else if (strcmp(cur, "forkproxy") == 0) {
 		forkproxy(buf, cur, size);
 	}

From e763c96b9fcd8245cd62af37ea57825a890c76b0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Sat, 10 Mar 2018 13:17:02 +0100
Subject: [PATCH 07/19] lxd: Drop ADVANCE_ARG_REQUIRED
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/main_forkfile.go | 47 +++++++++++++++++-------------------------
 lxd/main_forknet.go  | 24 ++++++++--------------
 lxd/main_nsexec.go   | 58 ++++++++++++++++++++++++++++++----------------------
 3 files changed, 60 insertions(+), 69 deletions(-)

diff --git a/lxd/main_forkfile.go b/lxd/main_forkfile.go
index c1f3c9a97..eb6650295 100644
--- a/lxd/main_forkfile.go
+++ b/lxd/main_forkfile.go
@@ -18,17 +18,7 @@ import (
 #include <sys/stat.h>
 #include <unistd.h>
 
-#define ADVANCE_ARG_REQUIRED() \
-	do { \
-		while (*cur != 0) \
-			cur++; \
-		cur++; \
-		if (size <= cur - buf) { \
-			fprintf(stderr, "not enough arguments\n"); \
-			_exit(1); \
-		} \
-	} while(0)
-
+extern bool advance_arg(char *buf, char *cur, ssize_t size, bool required);
 extern void error(char *msg);
 extern void attach_userns(int pid);
 extern int dosetns(int pid, char *nstype);
@@ -295,14 +285,14 @@ void forkdofile(char *buf, char *cur, ssize_t size, bool is_put, char *rootfs, p
 	bool append = false;
 
 
-	ADVANCE_ARG_REQUIRED();
+	advance_arg(buf, cur, size, true);
 	if (is_put) {
 		source = cur;
 	} else {
 		target = cur;
 	}
 
-	ADVANCE_ARG_REQUIRED();
+	advance_arg(buf, cur, size, true);
 	if (is_put) {
 		target = cur;
 	} else {
@@ -310,28 +300,28 @@ void forkdofile(char *buf, char *cur, ssize_t size, bool is_put, char *rootfs, p
 	}
 
 	if (is_put) {
-		ADVANCE_ARG_REQUIRED();
+		advance_arg(buf, cur, size, true);
 		type = cur;
 
-		ADVANCE_ARG_REQUIRED();
+		advance_arg(buf, cur, size, true);
 		uid = atoi(cur);
 
-		ADVANCE_ARG_REQUIRED();
+		advance_arg(buf, cur, size, true);
 		gid = atoi(cur);
 
-		ADVANCE_ARG_REQUIRED();
+		advance_arg(buf, cur, size, true);
 		mode = atoi(cur);
 
-		ADVANCE_ARG_REQUIRED();
+		advance_arg(buf, cur, size, true);
 		defaultUid = atoi(cur);
 
-		ADVANCE_ARG_REQUIRED();
+		advance_arg(buf, cur, size, true);
 		defaultGid = atoi(cur);
 
-		ADVANCE_ARG_REQUIRED();
+		advance_arg(buf, cur, size, true);
 		defaultMode = atoi(cur);
 
-		ADVANCE_ARG_REQUIRED();
+		advance_arg(buf, cur, size, true);
 		if (strcmp(cur, "append") == 0) {
 			append = true;
 		}
@@ -345,7 +335,7 @@ void forkdofile(char *buf, char *cur, ssize_t size, bool is_put, char *rootfs, p
 void forkcheckfile(char *buf, char *cur, ssize_t size, char *rootfs, pid_t pid) {
 	char *path = NULL;
 
-	ADVANCE_ARG_REQUIRED();
+	advance_arg(buf, cur, size, true);
 	path = cur;
 
 	if (pid > 0) {
@@ -379,7 +369,7 @@ void forkremovefile(char *buf, char *cur, ssize_t size, char *rootfs, pid_t pid)
 	char *path = NULL;
 	struct stat sb;
 
-	ADVANCE_ARG_REQUIRED();
+	advance_arg(buf, cur, size, true);
 	path = cur;
 
 	if (pid > 0) {
@@ -427,19 +417,20 @@ void forkfile(char *buf, char *cur, ssize_t size) {
 	pid_t pid = 0;
 
 	// Get the subcommand
-	ADVANCE_ARG_REQUIRED();
-	command = cur;
+	if (advance_arg(buf, cur, size, false)) {
+		command = cur;
+	}
 
 	// Get the container rootfs
-	ADVANCE_ARG_REQUIRED();
-	if (strcmp(cur, "--help") == 0 || strcmp(cur, "--version") == 0 || strcmp(cur, "-h") == 0) {
+	advance_arg(buf, cur, size, false);
+	if (command == NULL || (strcmp(cur, "--help") == 0 || strcmp(cur, "--version") == 0 || strcmp(cur, "-h") == 0)) {
 		return;
 	}
 
 	rootfs = cur;
 
 	// Get the container PID
-	ADVANCE_ARG_REQUIRED();
+	advance_arg(buf, cur, size, true);
 	pid = atoi(cur);
 
 	// Check that we're root
diff --git a/lxd/main_forknet.go b/lxd/main_forknet.go
index f019c63f6..cadd714fb 100644
--- a/lxd/main_forknet.go
+++ b/lxd/main_forknet.go
@@ -16,23 +16,14 @@ import (
 /*
 #define _GNU_SOURCE
 #include <errno.h>
+#include <stdbool.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <sys/types.h>
 #include <unistd.h>
 
-#define ADVANCE_ARG_REQUIRED() \
-	do { \
-		while (*cur != 0) \
-			cur++; \
-		cur++; \
-		if (size <= cur - buf) { \
-			fprintf(stderr, "not enough arguments\n"); \
-			_exit(1); \
-		} \
-	} while(0)
-
+extern bool advance_arg(char *buf, char *cur, ssize_t size, bool required);
 extern int dosetns(int pid, char *nstype);
 
 void forkdonetinfo(char *buf, char *cur, ssize_t size, pid_t pid) {
@@ -49,12 +40,13 @@ void forknet(char *buf, char *cur, ssize_t size) {
 	pid_t pid = 0;
 
 	// Get the subcommand
-	ADVANCE_ARG_REQUIRED();
-	command = cur;
+	if (advance_arg(buf, cur, size, false)) {
+		command = cur;
+	}
 
 	// Get the pid
-	ADVANCE_ARG_REQUIRED();
-	if (strcmp(cur, "--help") == 0 || strcmp(cur, "--version") == 0 || strcmp(cur, "-h") == 0) {
+	advance_arg(buf, cur, size, false);
+	if (command == NULL || (strcmp(cur, "--help") == 0 || strcmp(cur, "--version") == 0 || strcmp(cur, "-h") == 0)) {
 		return;
 	}
 
@@ -62,7 +54,7 @@ void forknet(char *buf, char *cur, ssize_t size) {
 
 	// Check that we're root
 	if (geteuid() != 0) {
-		fprintf(stderr, "Error: forkfile requires root privileges\n");
+		fprintf(stderr, "Error: forknet requires root privileges\n");
 		_exit(1);
 	}
 
diff --git a/lxd/main_nsexec.go b/lxd/main_nsexec.go
index a94be06f2..b05d9e936 100644
--- a/lxd/main_nsexec.go
+++ b/lxd/main_nsexec.go
@@ -27,6 +27,7 @@ package main
 #include <libgen.h>
 #include <linux/limits.h>
 #include <sched.h>
+#include <stdbool.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -37,20 +38,25 @@ package main
 
 #define CMDLINE_SIZE (8 * PATH_MAX)
 
-#define ADVANCE_ARG_REQUIRED() \
-	do { \
-		while (*cur != 0) \
-			cur++; \
-		cur++; \
-		if (size <= cur - buf) { \
-			fprintf(stderr, "not enough arguments\n"); \
-			_exit(1); \
-		} \
-	} while(0)
-
 extern void forkfile(char *buf, char *cur, ssize_t size);
 extern void forknet(char *buf, char *cur, ssize_t size);
 
+bool advance_arg(char *buf, char *cur, ssize_t size, bool required) {
+	while (*cur != 0)
+		cur++;
+
+	cur++;
+	if (size <= cur - buf) {
+		if (!required)
+			return false;
+
+		fprintf(stderr, "not enough arguments\n");
+		_exit(1);
+	}
+
+	return true;
+}
+
 void error(char *msg)
 {
 	int old_errno = errno;
@@ -224,7 +230,7 @@ void create(char *src, char *dest) {
 void forkmount(char *buf, char *cur, ssize_t size) {
 	char *src, *dest, *opts;
 
-	ADVANCE_ARG_REQUIRED();
+	advance_arg(buf, cur, size, true);
 	int pid = atoi(cur);
 
 	attach_userns(pid);
@@ -234,10 +240,10 @@ void forkmount(char *buf, char *cur, ssize_t size) {
 		_exit(1);
 	}
 
-	ADVANCE_ARG_REQUIRED();
+	advance_arg(buf, cur, size, true);
 	src = cur;
 
-	ADVANCE_ARG_REQUIRED();
+	advance_arg(buf, cur, size, true);
 	dest = cur;
 
 	create(src, dest);
@@ -264,15 +270,17 @@ void forkmount(char *buf, char *cur, ssize_t size) {
 }
 
 void forkumount(char *buf, char *cur, ssize_t size) {
-	ADVANCE_ARG_REQUIRED();
-	int pid = atoi(cur);
+	pid_t pid;
+
+	advance_arg(buf, cur, size, true);
+	pid = atoi(cur);
 
 	if (dosetns(pid, "mnt") < 0) {
 		fprintf(stderr, "Failed setns to container mount namespace: %s\n", strerror(errno));
 		_exit(1);
 	}
 
-	ADVANCE_ARG_REQUIRED();
+	advance_arg(buf, cur, size, true);
 	if (access(cur, F_OK) < 0) {
 		fprintf(stderr, "Mount path doesn't exist: %s\n", strerror(errno));
 		_exit(1);
@@ -292,19 +300,19 @@ void forkproxy(char *buf, char *cur, ssize_t size) {
 	FILE *logFile = NULL, *pidFile = NULL;
 
 	// Get the arguments
-	ADVANCE_ARG_REQUIRED();
+	advance_arg(buf, cur, size, true);
 	listen_pid = atoi(cur);
-	ADVANCE_ARG_REQUIRED();
-	ADVANCE_ARG_REQUIRED();
+	advance_arg(buf, cur, size, true);
+	advance_arg(buf, cur, size, true);
 	connect_pid = atoi(cur);
-	ADVANCE_ARG_REQUIRED();
-	ADVANCE_ARG_REQUIRED();
+	advance_arg(buf, cur, size, true);
+	advance_arg(buf, cur, size, true);
 	fdnum = atoi(cur);
-	ADVANCE_ARG_REQUIRED();
+	advance_arg(buf, cur, size, true);
 	forked = atoi(cur);
-	ADVANCE_ARG_REQUIRED();
+	advance_arg(buf, cur, size, true);
 	logPath = cur;
-	ADVANCE_ARG_REQUIRED();
+	advance_arg(buf, cur, size, true);
 	pidPath = cur;
 
 	// Check if proxy daemon already forked

From cf7fc0aa92e49cc1cadccf74edeb002ab80a5aea Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Sat, 10 Mar 2018 13:43:37 +0100
Subject: [PATCH 08/19] lxd: Port forkexec to cobra
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/main.go          |  4 +++
 lxd/main_forkexec.go | 79 ++++++++++++++++++++++++++++++++++------------------
 2 files changed, 56 insertions(+), 27 deletions(-)

diff --git a/lxd/main.go b/lxd/main.go
index 496a7e715..a3e819276 100644
--- a/lxd/main.go
+++ b/lxd/main.go
@@ -84,6 +84,10 @@ func main() {
 	callhookCmd := cmdCallhook{global: &globalCmd}
 	app.AddCommand(callhookCmd.Command())
 
+	// forkexec sub-command
+	forkexecCmd := cmdForkexec{global: &globalCmd}
+	app.AddCommand(forkexecCmd.Command())
+
 	// forkfile sub-command
 	forkfileCmd := cmdForkfile{global: &globalCmd}
 	app.AddCommand(forkfileCmd.Command())
diff --git a/lxd/main_forkexec.go b/lxd/main_forkexec.go
index 83d0e41a3..455348713 100644
--- a/lxd/main_forkexec.go
+++ b/lxd/main_forkexec.go
@@ -2,38 +2,63 @@ package main
 
 import (
 	"encoding/json"
+	"fmt"
 	"os"
 	"strings"
 	"syscall"
 
+	"github.com/spf13/cobra"
 	"gopkg.in/lxc/go-lxc.v2"
 
 	"github.com/lxc/lxd/shared"
 )
 
-/*
- * This is called by lxd when called as "lxd forkexec <container>"
- */
-func cmdForkExec(args *Args) error {
-	if len(args.Params) < 3 {
-		return SubCommandErrorf(-1, "Bad params: %q", args.Params)
-	}
-	if len(args.Extra) < 1 {
-		return SubCommandErrorf(-1, "Bad extra: %q", args.Extra)
+type cmdForkexec struct {
+	cmd    *cobra.Command
+	global *cmdGlobal
+}
+
+func (c *cmdForkexec) Command() *cobra.Command {
+	// Main subcommand
+	cmd := &cobra.Command{}
+	cmd.Use = "forkexec <container name> <containers path> <config> -- env [key=value...] -- cmd <args...>"
+	cmd.Short = "Execute a task inside the container"
+	cmd.Long = `Description:
+  Execute a task inside the container
+
+  This internal command is used to spawn a task inside the container and
+  allow LXD to interact with it.
+`
+	cmd.RunE = c.Run
+	cmd.Hidden = true
+
+	c.cmd = cmd
+	return cmd
+}
+
+func (c *cmdForkexec) Run(cmd *cobra.Command, args []string) error {
+	if len(args) < 4 {
+		cmd.Help()
+
+		if len(args) == 0 {
+			return nil
+		}
+
+		return fmt.Errorf("Missing required arguments")
 	}
 
-	name := args.Params[0]
-	lxcpath := args.Params[1]
-	configPath := args.Params[2]
+	name := args[0]
+	lxcpath := args[1]
+	configPath := args[2]
 
-	c, err := lxc.NewContainer(name, lxcpath)
+	d, err := lxc.NewContainer(name, lxcpath)
 	if err != nil {
-		return SubCommandErrorf(-1, "Error initializing container for start: %q", err)
+		return fmt.Errorf("Error initializing container for start: %q", err)
 	}
 
-	err = c.LoadConfigFile(configPath)
+	err = d.LoadConfigFile(configPath)
 	if err != nil {
-		return SubCommandErrorf(-1, "Error opening startup config file: %q", err)
+		return fmt.Errorf("Error opening startup config file: %q", err)
 	}
 
 	syscall.Dup3(int(os.Stdin.Fd()), 200, 0)
@@ -62,10 +87,10 @@ func cmdForkExec(args *Args) error {
 	}
 
 	env := []string{}
-	cmd := []string{}
+	command := []string{}
 
 	section := ""
-	for _, arg := range args.Extra {
+	for _, arg := range args[3:] {
 		// The "cmd" section must come last as it may contain a --
 		if arg == "--" && section != "cmd" {
 			section = ""
@@ -84,17 +109,17 @@ func cmdForkExec(args *Args) error {
 			}
 			env = append(env, arg)
 		} else if section == "cmd" {
-			cmd = append(cmd, arg)
+			command = append(command, arg)
 		} else {
-			return SubCommandErrorf(-1, "Invalid exec section: %s", section)
+			return fmt.Errorf("Invalid exec section: %s", section)
 		}
 	}
 
 	opts.Env = env
 
-	status, err := c.RunCommandNoWait(cmd, opts)
+	status, err := d.RunCommandNoWait(command, opts)
 	if err != nil {
-		return SubCommandErrorf(-1, "Failed running command: %q", err)
+		return fmt.Errorf("Failed running command: %q", err)
 	}
 	// Send the PID of the executing process.
 	w := os.NewFile(uintptr(3), "attachedPid")
@@ -102,23 +127,23 @@ func cmdForkExec(args *Args) error {
 
 	err = json.NewEncoder(w).Encode(status)
 	if err != nil {
-		return SubCommandErrorf(-1, "Failed sending PID of executing command: %q", err)
+		return fmt.Errorf("Failed sending PID of executing command: %q", err)
 	}
 
 	var ws syscall.WaitStatus
 	wpid, err := syscall.Wait4(status, &ws, 0, nil)
 	if err != nil || wpid != status {
-		return SubCommandErrorf(-1, "Failed finding process: %q", err)
+		return fmt.Errorf("Failed finding process: %q", err)
 	}
 
 	if ws.Exited() {
-		return SubCommandErrorf(ws.ExitStatus(), "")
+		os.Exit(ws.ExitStatus())
 	}
 
 	if ws.Signaled() {
 		// 128 + n == Fatal error signal "n"
-		return SubCommandErrorf(128+int(ws.Signal()), "")
+		os.Exit(128 + int(ws.Signal()))
 	}
 
-	return SubCommandErrorf(-1, "Command failed")
+	return fmt.Errorf("Command failed")
 }

From cf3cca9c1d6f7dbd0ecc1351fdd55dd79431a9fa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Sat, 10 Mar 2018 13:48:54 +0100
Subject: [PATCH 09/19] lxd: Port forkstart to cobra
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/main.go           |  4 ++++
 lxd/main_forkstart.go | 53 +++++++++++++++++++++++++++++++++++++--------------
 2 files changed, 43 insertions(+), 14 deletions(-)

diff --git a/lxd/main.go b/lxd/main.go
index a3e819276..d31030a59 100644
--- a/lxd/main.go
+++ b/lxd/main.go
@@ -96,6 +96,10 @@ func main() {
 	forknetCmd := cmdForknet{global: &globalCmd}
 	app.AddCommand(forknetCmd.Command())
 
+	// forkstart sub-command
+	forkstartCmd := cmdForkstart{global: &globalCmd}
+	app.AddCommand(forkstartCmd.Command())
+
 	// import sub-command
 	importCmd := cmdImport{global: &globalCmd}
 	app.AddCommand(importCmd.Command())
diff --git a/lxd/main_forkstart.go b/lxd/main_forkstart.go
index 0c51daa1e..55fc1d969 100644
--- a/lxd/main_forkstart.go
+++ b/lxd/main_forkstart.go
@@ -5,31 +5,56 @@ import (
 	"os"
 	"syscall"
 
+	"github.com/spf13/cobra"
 	"gopkg.in/lxc/go-lxc.v2"
 
 	"github.com/lxc/lxd/shared"
 )
 
-/*
- * This is called by lxd when called as "lxd forkstart <container>"
- * 'forkstart' is used instead of just 'start' in the hopes that people
- * do not accidentally type 'lxd start' instead of 'lxc start'
- */
-func cmdForkStart(args *Args) error {
-	if len(args.Params) != 3 {
-		return fmt.Errorf("Bad arguments: %q", args.Params)
+type cmdForkstart struct {
+	cmd    *cobra.Command
+	global *cmdGlobal
+}
+
+func (c *cmdForkstart) Command() *cobra.Command {
+	// Main subcommand
+	cmd := &cobra.Command{}
+	cmd.Use = "forkstart <container name> <containers path> <config>"
+	cmd.Short = "Start the container"
+	cmd.Long = `Description:
+  Start the container
+
+  This internal command is used to start the container as a separate
+  process.
+`
+	cmd.RunE = c.Run
+	cmd.Hidden = true
+
+	c.cmd = cmd
+	return cmd
+}
+
+func (c *cmdForkstart) Run(cmd *cobra.Command, args []string) error {
+	if len(args) != 3 {
+		cmd.Help()
+
+		if len(args) == 0 {
+			return nil
+		}
+
+		return fmt.Errorf("Missing required arguments")
 	}
 
-	name := args.Params[0]
-	lxcpath := args.Params[1]
-	configPath := args.Params[2]
+	name := args[0]
+	lxcpath := args[1]
+	configPath := args[2]
 
-	c, err := lxc.NewContainer(name, lxcpath)
+	d, err := lxc.NewContainer(name, lxcpath)
 	if err != nil {
 		return fmt.Errorf("Error initializing container for start: %q", err)
 	}
 
-	err = c.LoadConfigFile(configPath)
+	err = d.LoadConfigFile(configPath)
 	if err != nil {
 		return fmt.Errorf("Error opening startup config file: %q", err)
 	}
@@ -55,5 +80,5 @@ func cmdForkStart(args *Args) error {
 		syscall.Dup3(int(logFile.Fd()), 2, 0)
 	}
 
-	return c.Start()
+	return d.Start()
 }

From 4530d6a8ec72577a1891fb55a723c65225cbe337 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Sat, 10 Mar 2018 16:03:39 +0100
Subject: [PATCH 10/19] lxd: Port forkconsole to cobra
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/main.go             |  4 +++
 lxd/main_forkconsole.go | 66 +++++++++++++++++++++++++++++++++++--------------
 2 files changed, 51 insertions(+), 19 deletions(-)

diff --git a/lxd/main.go b/lxd/main.go
index d31030a59..94bb8cbf3 100644
--- a/lxd/main.go
+++ b/lxd/main.go
@@ -84,6 +84,10 @@ func main() {
 	callhookCmd := cmdCallhook{global: &globalCmd}
 	app.AddCommand(callhookCmd.Command())
 
+	// forkconsole sub-command
+	forkconsoleCmd := cmdForkconsole{global: &globalCmd}
+	app.AddCommand(forkconsoleCmd.Command())
+
 	// forkexec sub-command
 	forkexecCmd := cmdForkexec{global: &globalCmd}
 	app.AddCommand(forkexecCmd.Command())
diff --git a/lxd/main_forkconsole.go b/lxd/main_forkconsole.go
index d97000e2d..12ede5920 100644
--- a/lxd/main_forkconsole.go
+++ b/lxd/main_forkconsole.go
@@ -1,45 +1,73 @@
 package main
 
 import (
+	"fmt"
 	"os"
 	"strconv"
 	"strings"
 
+	"github.com/spf13/cobra"
+
 	"gopkg.in/lxc/go-lxc.v2"
 )
 
-/*
- * This is called by lxd when called as "lxd forkconsole <container> <path> <conf> <tty=<n>> <escape=<n>>"
- */
-func cmdForkConsole(args *Args) error {
-	if len(args.Params) != 5 {
-		return SubCommandErrorf(-1, "Bad params: %q", args.Params)
+type cmdForkconsole struct {
+	cmd    *cobra.Command
+	global *cmdGlobal
+}
+
+func (c *cmdForkconsole) Command() *cobra.Command {
+	// Main subcommand
+	cmd := &cobra.Command{}
+	cmd.Use = "forkconsole <container name> <containers path> <config> <tty> <escape>"
+	cmd.Short = "Attach to the console of a container"
+	cmd.Long = `Description:
+  Attach to the console of a container
+
+  This internal command is used to attach to one of the container's tty devices.
+`
+	cmd.RunE = c.Run
+	cmd.Hidden = true
+
+	c.cmd = cmd
+	return cmd
+}
+
+func (c *cmdForkconsole) Run(cmd *cobra.Command, args []string) error {
+	if len(args) != 5 {
+		cmd.Help()
+
+		if len(args) == 0 {
+			return nil
+		}
+
+		return fmt.Errorf("Missing required arguments")
 	}
 
-	name := args.Params[0]
-	lxcpath := args.Params[1]
-	configPath := args.Params[2]
+	name := args[0]
+	lxcpath := args[1]
+	configPath := args[2]
 
-	ttyNum := strings.TrimPrefix(args.Params[3], "tty=")
+	ttyNum := strings.TrimPrefix(args[3], "tty=")
 	tty, err := strconv.Atoi(ttyNum)
 	if err != nil {
-		return SubCommandErrorf(-1, "Failed to retrieve tty number: %q", err)
+		return fmt.Errorf("Failed to retrieve tty number: %q", err)
 	}
 
-	escapeNum := strings.TrimPrefix(args.Params[4], "escape=")
+	escapeNum := strings.TrimPrefix(args[4], "escape=")
 	escape, err := strconv.Atoi(escapeNum)
 	if err != nil {
-		return SubCommandErrorf(-1, "Failed to retrieve escape character: %q", err)
+		return fmt.Errorf("Failed to retrieve escape character: %q", err)
 	}
 
-	c, err := lxc.NewContainer(name, lxcpath)
+	d, err := lxc.NewContainer(name, lxcpath)
 	if err != nil {
-		return SubCommandErrorf(-1, "Error initializing container: %q", err)
+		return fmt.Errorf("Error initializing container: %q", err)
 	}
 
-	err = c.LoadConfigFile(configPath)
+	err = d.LoadConfigFile(configPath)
 	if err != nil {
-		return SubCommandErrorf(-1, "Error opening config file: %q", err)
+		return fmt.Errorf("Error opening config file: %q", err)
 	}
 
 	opts := lxc.ConsoleOptions{}
@@ -49,9 +77,9 @@ func cmdForkConsole(args *Args) error {
 	opts.StderrFd = uintptr(os.Stderr.Fd())
 	opts.EscapeCharacter = rune(escape)
 
-	err = c.Console(opts)
+	err = d.Console(opts)
 	if err != nil {
-		return SubCommandErrorf(-1, "Failed running forkconsole: %q", err)
+		return fmt.Errorf("Failed running forkconsole: %q", err)
 	}
 
 	return nil

From 9c7a4c9d132b304a1fc1787259ff7cf3115de88b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Sat, 10 Mar 2018 16:11:42 +0100
Subject: [PATCH 11/19] lxd: Port forkmigrate to cobra
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/main.go             |  4 ++++
 lxd/main_forkmigrate.go | 64 ++++++++++++++++++++++++++++++++-----------------
 2 files changed, 46 insertions(+), 22 deletions(-)

diff --git a/lxd/main.go b/lxd/main.go
index 94bb8cbf3..1f2ca5d07 100644
--- a/lxd/main.go
+++ b/lxd/main.go
@@ -96,6 +96,10 @@ func main() {
 	forkfileCmd := cmdForkfile{global: &globalCmd}
 	app.AddCommand(forkfileCmd.Command())
 
+	// forkmigrate sub-command
+	forkmigrateCmd := cmdForkmigrate{global: &globalCmd}
+	app.AddCommand(forkmigrateCmd.Command())
+
 	// forknet sub-command
 	forknetCmd := cmdForknet{global: &globalCmd}
 	app.AddCommand(forknetCmd.Command())
diff --git a/lxd/main_forkmigrate.go b/lxd/main_forkmigrate.go
index 00792635b..a812337ce 100644
--- a/lxd/main_forkmigrate.go
+++ b/lxd/main_forkmigrate.go
@@ -5,41 +5,61 @@ import (
 	"os"
 	"strconv"
 
+	"github.com/spf13/cobra"
+
 	"gopkg.in/lxc/go-lxc.v2"
 )
 
-/*
- * Similar to forkstart, this is called when lxd is invoked as:
- *
- *    lxd forkmigrate <container> <lxcpath> <path_to_config> <path_to_criu_images> <preserves_inodes>
- *
- * liblxc's restore() sets up the processes in such a way that the monitor ends
- * up being a child of the process that calls it, in our case lxd. However, we
- * really want the monitor to be daemonized, so we fork again. Additionally, we
- * want to fork for the same reasons we do forkstart (i.e. reduced memory
- * footprint when we fork tasks that will never free golang's memory, etc.)
- */
-func cmdForkMigrate(args *Args) error {
-	if len(args.Params) != 5 {
-		return fmt.Errorf("Bad arguments %q", args.Params)
+type cmdForkmigrate struct {
+	cmd    *cobra.Command
+	global *cmdGlobal
+}
+
+func (c *cmdForkmigrate) Command() *cobra.Command {
+	// Main subcommand
+	cmd := &cobra.Command{}
+	cmd.Use = "forkmigrate <container name> <containers path> <config> <images path> <preserve>"
+	cmd.Short = "Restore the container from saved state"
+	cmd.Long = `Description:
+  Restore the container from saved state
+
+  This internal command is used to start the container as a separate
+  process, restoring its recorded state.
+`
+	cmd.RunE = c.Run
+	cmd.Hidden = true
+
+	c.cmd = cmd
+	return cmd
+}
+
+func (c *cmdForkmigrate) Run(cmd *cobra.Command, args []string) error {
+	if len(args) != 5 {
+		cmd.Help()
+
+		if len(args) == 0 {
+			return nil
+		}
+
+		return fmt.Errorf("Missing required arguments")
 	}
 
-	name := args.Params[0]
-	lxcpath := args.Params[1]
-	configPath := args.Params[2]
-	imagesDir := args.Params[3]
+	name := args[0]
+	lxcpath := args[1]
+	configPath := args[2]
+	imagesDir := args[3]
 
-	preservesInodes, err := strconv.ParseBool(args.Params[4])
+	preservesInodes, err := strconv.ParseBool(args[4])
 	if err != nil {
 		return err
 	}
 
-	c, err := lxc.NewContainer(name, lxcpath)
+	d, err := lxc.NewContainer(name, lxcpath)
 	if err != nil {
 		return err
 	}
 
-	if err := c.LoadConfigFile(configPath); err != nil {
+	if err := d.LoadConfigFile(configPath); err != nil {
 		return err
 	}
 
@@ -48,7 +68,7 @@ func cmdForkMigrate(args *Args) error {
 	os.Stdout.Close()
 	os.Stderr.Close()
 
-	return c.Migrate(lxc.MIGRATE_RESTORE, lxc.MigrateOptions{
+	return d.Migrate(lxc.MIGRATE_RESTORE, lxc.MigrateOptions{
 		Directory:       imagesDir,
 		Verbose:         true,
 		PreservesInodes: preservesInodes,

From 7d7b18c2e968f0a00ce0db7a746a8bf94d4bd428 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Sat, 10 Mar 2018 10:37:39 -0500
Subject: [PATCH 12/19] lxd: Port forkmount to cobra
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/container_lxc.go  |   4 +-
 lxd/main_forkmount.go | 251 ++++++++++++++++++++++++++++++++++++++++++++++++++
 lxd/main_nsexec.go    | 177 ++---------------------------------
 3 files changed, 259 insertions(+), 173 deletions(-)
 create mode 100644 lxd/main_forkmount.go

diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index 56355e7d1..2b850da3e 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -5964,7 +5964,7 @@ func (c *containerLXC) insertMount(source, target, fstype string, flags int) err
 	mntsrc := filepath.Join("/dev/.lxd-mounts", filepath.Base(tmpMount))
 	pidStr := fmt.Sprintf("%d", pid)
 
-	out, err := shared.RunCommand(c.state.OS.ExecPath, "forkmount", pidStr, mntsrc, target)
+	out, err := shared.RunCommand(c.state.OS.ExecPath, "forkmount", "mount", pidStr, mntsrc, target)
 
 	if out != "" {
 		for _, line := range strings.Split(strings.TrimRight(out, "\n"), "\n") {
@@ -5989,7 +5989,7 @@ func (c *containerLXC) removeMount(mount string) error {
 
 	// Remove the mount from the container
 	pidStr := fmt.Sprintf("%d", pid)
-	out, err := shared.RunCommand(c.state.OS.ExecPath, "forkumount", pidStr, mount)
+	out, err := shared.RunCommand(c.state.OS.ExecPath, "forkmount", "umount", pidStr, mount)
 
 	if out != "" {
 		for _, line := range strings.Split(strings.TrimRight(out, "\n"), "\n") {
diff --git a/lxd/main_forkmount.go b/lxd/main_forkmount.go
new file mode 100644
index 000000000..a2b14c944
--- /dev/null
+++ b/lxd/main_forkmount.go
@@ -0,0 +1,251 @@
+package main
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+)
+
+/*
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <libgen.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+extern bool advance_arg(char *buf, char *cur, ssize_t size, bool required);
+extern void error(char *msg);
+extern void attach_userns(int pid);
+extern int dosetns(int pid, char *nstype);
+
+int mkdir_p(const char *dir, mode_t mode)
+{
+	const char *tmp = dir;
+	const char *orig = dir;
+	char *makeme;
+
+	do {
+		dir = tmp + strspn(tmp, "/");
+		tmp = dir + strcspn(dir, "/");
+		makeme = strndup(orig, dir - orig);
+		if (*makeme) {
+			if (mkdir(makeme, mode) && errno != EEXIST) {
+				fprintf(stderr, "failed to create directory '%s': %s\n", makeme, strerror(errno));
+				free(makeme);
+				return -1;
+			}
+		}
+		free(makeme);
+	} while(tmp != dir);
+
+	return 0;
+}
+
+void ensure_dir(char *dest) {
+	struct stat sb;
+	if (stat(dest, &sb) == 0) {
+		if ((sb.st_mode & S_IFMT) == S_IFDIR)
+			return;
+		if (unlink(dest) < 0) {
+			fprintf(stderr, "Failed to remove old %s: %s\n", dest, strerror(errno));
+			_exit(1);
+		}
+	}
+	if (mkdir(dest, 0755) < 0) {
+		fprintf(stderr, "Failed to mkdir %s: %s\n", dest, strerror(errno));
+		_exit(1);
+	}
+}
+
+void ensure_file(char *dest) {
+	struct stat sb;
+	int fd;
+
+	if (stat(dest, &sb) == 0) {
+		if ((sb.st_mode & S_IFMT) != S_IFDIR)
+			return;
+		if (rmdir(dest) < 0) {
+			fprintf(stderr, "Failed to remove old %s: %s\n", dest, strerror(errno));
+			_exit(1);
+		}
+	}
+
+	fd = creat(dest, 0755);
+	if (fd < 0) {
+		fprintf(stderr, "Failed to mkdir %s: %s\n", dest, strerror(errno));
+		_exit(1);
+	}
+	close(fd);
+}
+
+void create(char *src, char *dest) {
+	char *dirdup;
+	char *destdirname;
+
+	struct stat sb;
+	if (stat(src, &sb) < 0) {
+		fprintf(stderr, "source %s does not exist\n", src);
+		_exit(1);
+	}
+
+	dirdup = strdup(dest);
+	if (!dirdup)
+		_exit(1);
+
+	destdirname = dirname(dirdup);
+
+	if (mkdir_p(destdirname, 0755) < 0) {
+		fprintf(stderr, "failed to create path: %s\n", destdirname);
+		free(dirdup);
+		_exit(1);
+	}
+	free(dirdup);
+
+	switch (sb.st_mode & S_IFMT) {
+	case S_IFDIR:
+		ensure_dir(dest);
+		return;
+	default:
+		ensure_file(dest);
+		return;
+	}
+}
+
+void forkdomount(char *buf, char *cur, ssize_t size, pid_t pid) {
+	char *src, *dest, *opts;
+
+	attach_userns(pid);
+
+	if (dosetns(pid, "mnt") < 0) {
+		fprintf(stderr, "Failed setns to container mount namespace: %s\n", strerror(errno));
+		_exit(1);
+	}
+
+	advance_arg(buf, cur, size, true);
+	src = cur;
+
+	advance_arg(buf, cur, size, true);
+	dest = cur;
+
+	create(src, dest);
+
+	if (access(src, F_OK) < 0) {
+		fprintf(stderr, "Mount source doesn't exist: %s\n", strerror(errno));
+		_exit(1);
+	}
+
+	if (access(dest, F_OK) < 0) {
+		fprintf(stderr, "Mount destination doesn't exist: %s\n", strerror(errno));
+		_exit(1);
+	}
+
+	// Here, we always move recursively, because we sometimes allow
+	// recursive mounts. If the mount has no kids then it doesn't matter,
+	// but if it does, we want to move those too.
+	if (mount(src, dest, "none", MS_MOVE | MS_REC, NULL) < 0) {
+		fprintf(stderr, "Failed mounting %s onto %s: %s\n", src, dest, strerror(errno));
+		_exit(1);
+	}
+
+	_exit(0);
+}
+
+void forkdoumount(char *buf, char *cur, ssize_t size, pid_t pid) {
+	if (dosetns(pid, "mnt") < 0) {
+		fprintf(stderr, "Failed setns to container mount namespace: %s\n", strerror(errno));
+		_exit(1);
+	}
+
+	advance_arg(buf, cur, size, true);
+	if (access(cur, F_OK) < 0) {
+		fprintf(stderr, "Mount path doesn't exist: %s\n", strerror(errno));
+		_exit(1);
+	}
+
+	if (umount2(cur, MNT_DETACH) < 0) {
+		fprintf(stderr, "Error unmounting %s: %s\n", cur, strerror(errno));
+		_exit(1);
+	}
+	_exit(0);
+}
+
+void forkmount(char *buf, char *cur, ssize_t size) {
+	char *command = NULL;
+	char *rootfs = NULL;
+	pid_t pid = 0;
+
+	// Get the subcommand
+	if (advance_arg(buf, cur, size, false)) {
+		command = cur;
+	}
+
+	// Get the pid
+	advance_arg(buf, cur, size, false);
+	if (command == NULL || (strcmp(cur, "--help") == 0 || strcmp(cur, "--version") == 0 || strcmp(cur, "-h") == 0)) {
+		return;
+	}
+
+	pid = atoi(cur);
+
+	// Check that we're root
+	if (geteuid() != 0) {
+		fprintf(stderr, "Error: forkmount requires root privileges\n");
+		_exit(1);
+	}
+
+	// Call the subcommands
+	if (strcmp(command, "mount") == 0) {
+		forkdomount(buf, cur, size, pid);
+	} else if (strcmp(command, "umount") == 0) {
+		forkdoumount(buf, cur, size, pid);
+	}
+}
+*/
+import "C"
+
+type cmdForkmount struct {
+	cmd    *cobra.Command
+	global *cmdGlobal
+}
+
+func (c *cmdForkmount) Command() *cobra.Command {
+	// Main subcommand
+	cmd := &cobra.Command{}
+	cmd.Use = "forkmount"
+	cmd.Short = "Perform mount operations"
+	cmd.Long = `Description:
+  Perform mount operations
+
+  This set of internal commands are used for all container mount
+  operations.
+`
+	cmd.Hidden = true
+
+	// mount
+	cmdMount := &cobra.Command{}
+	cmdMount.Use = "mount <PID> <source> <destination>"
+	cmdMount.Args = cobra.ExactArgs(3)
+	cmdMount.RunE = c.Run
+	cmd.AddCommand(cmdMount)
+
+	// umount
+	cmdUmount := &cobra.Command{}
+	cmdUmount.Use = "umount <PID> <path>"
+	cmdUmount.Args = cobra.ExactArgs(2)
+	cmdUmount.RunE = c.Run
+	cmd.AddCommand(cmdUmount)
+
+	c.cmd = cmd
+	return cmd
+}
+
+func (c *cmdForkmount) Run(cmd *cobra.Command, args []string) error {
+	return fmt.Errorf("This command should have been intercepted in cgo")
+}
diff --git a/lxd/main_nsexec.go b/lxd/main_nsexec.go
index b05d9e936..c42272b19 100644
--- a/lxd/main_nsexec.go
+++ b/lxd/main_nsexec.go
@@ -20,25 +20,21 @@ package main
 
 /*
 #define _GNU_SOURCE
-#include <dirent.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <grp.h>
-#include <libgen.h>
 #include <linux/limits.h>
 #include <sched.h>
 #include <stdbool.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
-#include <sys/mount.h>
-#include <sys/stat.h>
-#include <sys/types.h>
 #include <unistd.h>
 
 #define CMDLINE_SIZE (8 * PATH_MAX)
 
 extern void forkfile(char *buf, char *cur, ssize_t size);
+extern void forkmount(char *buf, char *cur, ssize_t size);
 extern void forknet(char *buf, char *cur, ssize_t size);
 
 bool advance_arg(char *buf, char *cur, ssize_t size, bool required) {
@@ -71,29 +67,6 @@ void error(char *msg)
 	fprintf(stderr, "errno: %d\n", old_errno);
 }
 
-int mkdir_p(const char *dir, mode_t mode)
-{
-	const char *tmp = dir;
-	const char *orig = dir;
-	char *makeme;
-
-	do {
-		dir = tmp + strspn(tmp, "/");
-		tmp = dir + strcspn(dir, "/");
-		makeme = strndup(orig, dir - orig);
-		if (*makeme) {
-			if (mkdir(makeme, mode) && errno != EEXIST) {
-				fprintf(stderr, "failed to create directory '%s': %s\n", makeme, strerror(errno));
-				free(makeme);
-				return -1;
-			}
-		}
-		free(makeme);
-	} while(tmp != dir);
-
-	return 0;
-}
-
 int dosetns(int pid, char *nstype) {
 	int mntns;
 	char buf[PATH_MAX];
@@ -117,22 +90,22 @@ int dosetns(int pid, char *nstype) {
 
 void attach_userns(int pid) {
 	char nspath[PATH_MAX];
-	char userns_source[PATH_MAX];
-	char userns_target[PATH_MAX];
+	char userns_source[17];
+	char userns_target[17];
 
 	sprintf(nspath, "/proc/%d/ns/user", pid);
 	if (access(nspath, F_OK) == 0) {
-		if (readlink("/proc/self/ns/user", userns_source, 18) < 0) {
+		if (readlink("/proc/self/ns/user", userns_source, 17) < 0) {
 			fprintf(stderr, "Failed readlink of source namespace: %s\n", strerror(errno));
 			_exit(1);
 		}
 
-		if (readlink(nspath, userns_target, PATH_MAX) < 0) {
+		if (readlink(nspath, userns_target, 17) < 0) {
 			fprintf(stderr, "Failed readlink of target namespace: %s\n", strerror(errno));
 			_exit(1);
 		}
 
-		if (strncmp(userns_source, userns_target, PATH_MAX) != 0) {
+		if (strncmp(userns_source, userns_target, 17) != 0) {
 			if (dosetns(pid, "user") < 0) {
 				fprintf(stderr, "Failed setns to container user namespace: %s\n", strerror(errno));
 				_exit(1);
@@ -157,142 +130,6 @@ void attach_userns(int pid) {
 	}
 }
 
-void ensure_dir(char *dest) {
-	struct stat sb;
-	if (stat(dest, &sb) == 0) {
-		if ((sb.st_mode & S_IFMT) == S_IFDIR)
-			return;
-		if (unlink(dest) < 0) {
-			fprintf(stderr, "Failed to remove old %s: %s\n", dest, strerror(errno));
-			_exit(1);
-		}
-	}
-	if (mkdir(dest, 0755) < 0) {
-		fprintf(stderr, "Failed to mkdir %s: %s\n", dest, strerror(errno));
-		_exit(1);
-	}
-}
-
-void ensure_file(char *dest) {
-	struct stat sb;
-	int fd;
-
-	if (stat(dest, &sb) == 0) {
-		if ((sb.st_mode & S_IFMT) != S_IFDIR)
-			return;
-		if (rmdir(dest) < 0) {
-			fprintf(stderr, "Failed to remove old %s: %s\n", dest, strerror(errno));
-			_exit(1);
-		}
-	}
-
-	fd = creat(dest, 0755);
-	if (fd < 0) {
-		fprintf(stderr, "Failed to mkdir %s: %s\n", dest, strerror(errno));
-		_exit(1);
-	}
-	close(fd);
-}
-
-void create(char *src, char *dest) {
-	char *dirdup;
-	char *destdirname;
-
-	struct stat sb;
-	if (stat(src, &sb) < 0) {
-		fprintf(stderr, "source %s does not exist\n", src);
-		_exit(1);
-	}
-
-	dirdup = strdup(dest);
-	if (!dirdup)
-		_exit(1);
-
-	destdirname = dirname(dirdup);
-
-	if (mkdir_p(destdirname, 0755) < 0) {
-		fprintf(stderr, "failed to create path: %s\n", destdirname);
-		free(dirdup);
-		_exit(1);
-	}
-	free(dirdup);
-
-	switch (sb.st_mode & S_IFMT) {
-	case S_IFDIR:
-		ensure_dir(dest);
-		return;
-	default:
-		ensure_file(dest);
-		return;
-	}
-}
-
-void forkmount(char *buf, char *cur, ssize_t size) {
-	char *src, *dest, *opts;
-
-	advance_arg(buf, cur, size, true);
-	int pid = atoi(cur);
-
-	attach_userns(pid);
-
-	if (dosetns(pid, "mnt") < 0) {
-		fprintf(stderr, "Failed setns to container mount namespace: %s\n", strerror(errno));
-		_exit(1);
-	}
-
-	advance_arg(buf, cur, size, true);
-	src = cur;
-
-	advance_arg(buf, cur, size, true);
-	dest = cur;
-
-	create(src, dest);
-
-	if (access(src, F_OK) < 0) {
-		fprintf(stderr, "Mount source doesn't exist: %s\n", strerror(errno));
-		_exit(1);
-	}
-
-	if (access(dest, F_OK) < 0) {
-		fprintf(stderr, "Mount destination doesn't exist: %s\n", strerror(errno));
-		_exit(1);
-	}
-
-	// Here, we always move recursively, because we sometimes allow
-	// recursive mounts. If the mount has no kids then it doesn't matter,
-	// but if it does, we want to move those too.
-	if (mount(src, dest, "none", MS_MOVE | MS_REC, NULL) < 0) {
-		fprintf(stderr, "Failed mounting %s onto %s: %s\n", src, dest, strerror(errno));
-		_exit(1);
-	}
-
-	_exit(0);
-}
-
-void forkumount(char *buf, char *cur, ssize_t size) {
-	pid_t pid;
-
-	advance_arg(buf, cur, size, true);
-	pid = atoi(cur);
-
-	if (dosetns(pid, "mnt") < 0) {
-		fprintf(stderr, "Failed setns to container mount namespace: %s\n", strerror(errno));
-		_exit(1);
-	}
-
-	advance_arg(buf, cur, size, true);
-	if (access(cur, F_OK) < 0) {
-		fprintf(stderr, "Mount path doesn't exist: %s\n", strerror(errno));
-		_exit(1);
-	}
-
-	if (umount2(cur, MNT_DETACH) < 0) {
-		fprintf(stderr, "Error unmounting %s: %s\n", cur, strerror(errno));
-		_exit(1);
-	}
-	_exit(0);
-}
-
 void forkproxy(char *buf, char *cur, ssize_t size) {
 	int cmdline, listen_pid, connect_pid, fdnum, forked, childPid, ret;
 	char fdpath[80];
@@ -417,8 +254,6 @@ __attribute__((constructor)) void init(void) {
 		forkfile(buf, cur, size);
 	} else if (strcmp(cur, "forkmount") == 0) {
 		forkmount(buf, cur, size);
-	} else if (strcmp(cur, "forkumount") == 0) {
-		forkumount(buf, cur, size);
 	} else if (strcmp(cur, "forknet") == 0) {
 		forknet(buf, cur, size);
 	} else if (strcmp(cur, "forkproxy") == 0) {

From 1a6722d88780b41dae796b49e5a2f17bc6ce350e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Sat, 10 Mar 2018 10:52:06 -0500
Subject: [PATCH 13/19] lxd: Port forkproxy to cobra
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/main.go           |   4 +
 lxd/main_forkproxy.go | 266 ++++++++++++++++++++++++++++++++++++++++++++++++++
 lxd/main_nsexec.go    |  91 +----------------
 lxd/main_proxy.go     | 130 ------------------------
 4 files changed, 271 insertions(+), 220 deletions(-)
 create mode 100644 lxd/main_forkproxy.go
 delete mode 100644 lxd/main_proxy.go

diff --git a/lxd/main.go b/lxd/main.go
index 1f2ca5d07..967e7c772 100644
--- a/lxd/main.go
+++ b/lxd/main.go
@@ -104,6 +104,10 @@ func main() {
 	forknetCmd := cmdForknet{global: &globalCmd}
 	app.AddCommand(forknetCmd.Command())
 
+	// forkproxy sub-command
+	forkproxyCmd := cmdForkproxy{global: &globalCmd}
+	app.AddCommand(forkproxyCmd.Command())
+
 	// forkstart sub-command
 	forkstartCmd := cmdForkstart{global: &globalCmd}
 	app.AddCommand(forkstartCmd.Command())
diff --git a/lxd/main_forkproxy.go b/lxd/main_forkproxy.go
new file mode 100644
index 000000000..08b05a104
--- /dev/null
+++ b/lxd/main_forkproxy.go
@@ -0,0 +1,266 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"net"
+	"os"
+	"strconv"
+	"strings"
+	"syscall"
+
+	"github.com/spf13/cobra"
+
+	"github.com/lxc/lxd/lxd/util"
+	"github.com/lxc/lxd/shared"
+)
+
+/*
+#define _GNU_SOURCE
+#include <errno.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+extern bool advance_arg(char *buf, char *cur, ssize_t size, bool required);
+extern int dosetns(int pid, char *nstype);
+
+void forkproxy(char *buf, char *cur, ssize_t size) {
+	int cmdline, listen_pid, connect_pid, fdnum, forked, childPid, ret;
+	char fdpath[80];
+	char *logPath = NULL, *pidPath = NULL;
+	FILE *logFile = NULL, *pidFile = NULL;
+
+	// Get the arguments
+	advance_arg(buf, cur, size, true);
+	listen_pid = atoi(cur);
+	advance_arg(buf, cur, size, true);
+	advance_arg(buf, cur, size, true);
+	connect_pid = atoi(cur);
+	advance_arg(buf, cur, size, true);
+	advance_arg(buf, cur, size, true);
+	fdnum = atoi(cur);
+	advance_arg(buf, cur, size, true);
+	forked = atoi(cur);
+	advance_arg(buf, cur, size, true);
+	logPath = cur;
+	advance_arg(buf, cur, size, true);
+	pidPath = cur;
+
+	// Check if proxy daemon already forked
+	if (forked == 0) {
+		logFile = fopen(logPath, "w+");
+		if (logFile == NULL) {
+			_exit(1);
+		}
+
+		if (dup2(fileno(logFile), STDOUT_FILENO) < 0) {
+			fprintf(logFile, "Failed to redirect STDOUT to logfile: %s\n", strerror(errno));
+			_exit(1);
+		}
+		if (dup2(fileno(logFile), STDERR_FILENO) < 0) {
+			fprintf(logFile, "Failed to redirect STDERR to logfile: %s\n", strerror(errno));
+			_exit(1);
+		}
+		fclose(logFile);
+
+		pidFile = fopen(pidPath, "w+");
+		if (pidFile == NULL) {
+			fprintf(stderr, "Failed to create pid file for proxy daemon: %s\n", strerror(errno));
+			_exit(1);
+		}
+
+		childPid = fork();
+		if (childPid < 0) {
+			fprintf(stderr, "Failed to fork proxy daemon: %s\n", strerror(errno));
+			_exit(1);
+		} else if (childPid != 0) {
+			fprintf(pidFile, "%d", childPid);
+			fclose(pidFile);
+			fclose(stdin);
+			fclose(stdout);
+			fclose(stderr);
+			_exit(0);
+		} else {
+			ret = setsid();
+			if (ret < 0) {
+				fprintf(stderr, "Failed to setsid in proxy daemon: %s\n", strerror(errno));
+				_exit(1);
+			}
+		}
+	}
+
+	// Cannot pass through -1 to runCommand since it is interpreted as a flag
+	fdnum = fdnum == 0 ? -1 : fdnum;
+
+	ret = snprintf(fdpath, sizeof(fdpath), "/proc/self/fd/%d", fdnum);
+	if (ret < 0 || (size_t)ret >= sizeof(fdpath)) {
+		fprintf(stderr, "Failed to format file descriptor path\n");
+		_exit(1);
+	}
+
+	// Join the listener ns if not already setup
+	if (access(fdpath, F_OK) < 0) {
+		// Attach to the network namespace of the listener
+		if (dosetns(listen_pid, "net") < 0) {
+			fprintf(stderr, "Failed setns to listener network namespace: %s\n", strerror(errno));
+			_exit(1);
+		}
+	} else {
+		// Join the connector ns now
+		if (dosetns(connect_pid, "net") < 0) {
+			fprintf(stderr, "Failed setns to connector network namespace: %s\n", strerror(errno));
+			_exit(1);
+		}
+	}
+}
+*/
+import "C"
+
+type cmdForkproxy struct {
+	cmd    *cobra.Command
+	global *cmdGlobal
+}
+
+func (c *cmdForkproxy) Command() *cobra.Command {
+	// Main subcommand
+	cmd := &cobra.Command{}
+	cmd.Use = "forkproxy <listen PID> <listen address> <connect PID> <connect address> <fd> <reexec> <log path> <pid path>"
+	cmd.Short = "Setup network connection proxying"
+	cmd.Long = `Description:
+  Setup network connection proxying
+
+  This internal command will spawn a new proxy process for a particular
+  container, connecting one side to the host and the other to the
+  container.
+`
+	cmd.RunE = c.Run
+	cmd.Hidden = true
+
+	c.cmd = cmd
+	return cmd
+}
+
+func (c *cmdForkproxy) Run(cmd *cobra.Command, args []string) error {
+	if len(args) != 8 {
+		cmd.Help()
+
+		if len(args) == 0 {
+			return nil
+		}
+
+		return fmt.Errorf("Missing required arguments")
+	}
+
+	// Get all our arguments
+	listenPid := args[0]
+	listenAddr := args[1]
+	connectPid := args[2]
+	connectAddr := args[3]
+
+	fd := -1
+	if args[4] != "0" {
+		fd, _ = strconv.Atoi(args[4])
+	}
+
+	// At this point we have already forked and should set this flag to 1
+	args[5] = "1"
+
+	// Check where we are in initialization
+	if !shared.PathExists(fmt.Sprintf("/proc/self/fd/%d", fd)) {
+		fmt.Printf("Listening on %s in %s, forwarding to %s from %s\n", listenAddr, listenPid, connectAddr, connectPid)
+
+		file, err := getListenerFile(listenAddr)
+		if err != nil {
+			return err
+		}
+		defer file.Close()
+
+		listenerFd := file.Fd()
+		if err != nil {
+			return fmt.Errorf("failed to duplicate the listener fd: %v", err)
+		}
+
+		newFd, err := syscall.Dup(int(listenerFd))
+		if err != nil {
+			return fmt.Errorf("failed to dup fd: %v", err)
+		}
+
+		fmt.Printf("Re-executing proxy process\n")
+
+		args[4] = strconv.Itoa(int(newFd))
+		execArgs := append([]string{"lxd", "forkproxy"}, args...)
+
+		err = syscall.Exec(util.GetExecPath(), execArgs, os.Environ())
+		if err != nil {
+			return fmt.Errorf("failed to re-exec: %v", err)
+		}
+	}
+
+	// Re-create listener from fd
+	listenFile := os.NewFile(uintptr(fd), "listener")
+	listener, err := net.FileListener(listenFile)
+	if err != nil {
+		return fmt.Errorf("failed to re-assemble listener: %v", err)
+	}
+
+	defer listener.Close()
+
+	fmt.Printf("Starting to proxy\n")
+
+	// begin proxying
+	for {
+		// Accept a new client
+		srcConn, err := listener.Accept()
+		if err != nil {
+			fmt.Printf("error: Failed to accept new connection: %v\n", err)
+			continue
+		}
+		fmt.Printf("Accepted a new connection\n")
+
+		// Connect to the target
+		dstConn, err := getDestConn(connectAddr)
+		if err != nil {
+			fmt.Printf("error: Failed to connect to target: %v\n", err)
+			srcConn.Close()
+			continue
+		}
+
+		go io.Copy(srcConn, dstConn)
+		go io.Copy(dstConn, srcConn)
+	}
+}
+
+func getListenerFile(listenAddr string) (os.File, error) {
+	fields := strings.SplitN(listenAddr, ":", 2)
+	addr := strings.Join(fields[1:], "")
+
+	listener, err := net.Listen(fields[0], addr)
+	if err != nil {
+		return os.File{}, err
+	}
+
+	file := &os.File{}
+	switch listener.(type) {
+	case *net.TCPListener:
+		tcpListener := listener.(*net.TCPListener)
+		file, err = tcpListener.File()
+	case *net.UnixListener:
+		unixListener := listener.(*net.UnixListener)
+		file, err = unixListener.File()
+	}
+
+	if err != nil {
+		return os.File{}, fmt.Errorf("Failed to get file from listener: %v", err)
+	}
+
+	return *file, nil
+}
+
+func getDestConn(connectAddr string) (net.Conn, error) {
+	fields := strings.SplitN(connectAddr, ":", 2)
+	addr := strings.Join(fields[1:], "")
+	return net.Dial(fields[0], addr)
+}
diff --git a/lxd/main_nsexec.go b/lxd/main_nsexec.go
index c42272b19..3699bb6cd 100644
--- a/lxd/main_nsexec.go
+++ b/lxd/main_nsexec.go
@@ -36,6 +36,7 @@ package main
 extern void forkfile(char *buf, char *cur, ssize_t size);
 extern void forkmount(char *buf, char *cur, ssize_t size);
 extern void forknet(char *buf, char *cur, ssize_t size);
+extern void forkproxy(char *buf, char *cur, ssize_t size);
 
 bool advance_arg(char *buf, char *cur, ssize_t size, bool required) {
 	while (*cur != 0)
@@ -130,96 +131,6 @@ void attach_userns(int pid) {
 	}
 }
 
-void forkproxy(char *buf, char *cur, ssize_t size) {
-	int cmdline, listen_pid, connect_pid, fdnum, forked, childPid, ret;
-	char fdpath[80];
-	char *logPath = NULL, *pidPath = NULL;
-	FILE *logFile = NULL, *pidFile = NULL;
-
-	// Get the arguments
-	advance_arg(buf, cur, size, true);
-	listen_pid = atoi(cur);
-	advance_arg(buf, cur, size, true);
-	advance_arg(buf, cur, size, true);
-	connect_pid = atoi(cur);
-	advance_arg(buf, cur, size, true);
-	advance_arg(buf, cur, size, true);
-	fdnum = atoi(cur);
-	advance_arg(buf, cur, size, true);
-	forked = atoi(cur);
-	advance_arg(buf, cur, size, true);
-	logPath = cur;
-	advance_arg(buf, cur, size, true);
-	pidPath = cur;
-
-	// Check if proxy daemon already forked
-	if (forked == 0) {
-		logFile = fopen(logPath, "w+");
-		if (logFile == NULL) {
-			_exit(1);
-		}
-
-		if (dup2(fileno(logFile), STDOUT_FILENO) < 0) {
-			fprintf(logFile, "Failed to redirect STDOUT to logfile: %s\n", strerror(errno));
-			_exit(1);
-		}
-		if (dup2(fileno(logFile), STDERR_FILENO) < 0) {
-			fprintf(logFile, "Failed to redirect STDERR to logfile: %s\n", strerror(errno));
-			_exit(1);
-		}
-		fclose(logFile);
-
-		pidFile = fopen(pidPath, "w+");
-		if (pidFile == NULL) {
-			fprintf(stderr, "Failed to create pid file for proxy daemon: %s\n", strerror(errno));
-			_exit(1);
-		}
-
-		childPid = fork();
-		if (childPid < 0) {
-			fprintf(stderr, "Failed to fork proxy daemon: %s\n", strerror(errno));
-			_exit(1);
-		} else if (childPid != 0) {
-			fprintf(pidFile, "%d", childPid);
-			fclose(pidFile);
-			fclose(stdin);
-			fclose(stdout);
-			fclose(stderr);
-			_exit(0);
-		} else {
-			ret = setsid();
-			if (ret < 0) {
-				fprintf(stderr, "Failed to setsid in proxy daemon: %s\n", strerror(errno));
-				_exit(1);
-			}
-		}
-	}
-
-	// Cannot pass through -1 to runCommand since it is interpreted as a flag
-	fdnum = fdnum == 0 ? -1 : fdnum;
-
-	ret = snprintf(fdpath, sizeof(fdpath), "/proc/self/fd/%d", fdnum);
-	if (ret < 0 || (size_t)ret >= sizeof(fdpath)) {
-		fprintf(stderr, "Failed to format file descriptor path\n");
-		_exit(1);
-	}
-
-	// Join the listener ns if not already setup
-	if (access(fdpath, F_OK) < 0) {
-		// Attach to the network namespace of the listener
-		if (dosetns(listen_pid, "net") < 0) {
-			fprintf(stderr, "Failed setns to listener network namespace: %s\n", strerror(errno));
-			_exit(1);
-		}
-	} else {
-		// Join the connector ns now
-		if (dosetns(connect_pid, "net") < 0) {
-			fprintf(stderr, "Failed setns to connector network namespace: %s\n", strerror(errno));
-			_exit(1);
-		}
-	}
-}
-
 __attribute__((constructor)) void init(void) {
 	int cmdline;
 	char buf[CMDLINE_SIZE];
diff --git a/lxd/main_proxy.go b/lxd/main_proxy.go
deleted file mode 100644
index 53cc1bb03..000000000
--- a/lxd/main_proxy.go
+++ /dev/null
@@ -1,130 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"io"
-	"net"
-	"os"
-	"strconv"
-	"strings"
-	"syscall"
-
-	"github.com/lxc/lxd/lxd/util"
-	"github.com/lxc/lxd/shared"
-)
-
-func cmdProxyDevStart(args *Args) error {
-	if len(args.Params) != 8 {
-		return fmt.Errorf("Invalid number of arguments")
-	}
-
-	// Get all our arguments
-	listenPid := args.Params[0]
-	listenAddr := args.Params[1]
-	connectPid := args.Params[2]
-	connectAddr := args.Params[3]
-
-	fd := -1
-	if args.Params[4] != "0" {
-		fd, _ = strconv.Atoi(args.Params[4])
-	}
-
-	// At this point we have already forked and should set this flag to 1
-	args.Params[5] = "1"
-
-	// Check where we are in initialization
-	if !shared.PathExists(fmt.Sprintf("/proc/self/fd/%d", fd)) {
-		fmt.Printf("Listening on %s in %s, forwarding to %s from %s\n", listenAddr, listenPid, connectAddr, connectPid)
-
-		file, err := getListenerFile(listenAddr)
-		if err != nil {
-			return err
-		}
-		defer file.Close()
-
-		listenerFd := file.Fd()
-		if err != nil {
-			return fmt.Errorf("failed to duplicate the listener fd: %v", err)
-		}
-
-		newFd, err := syscall.Dup(int(listenerFd))
-		if err != nil {
-			return fmt.Errorf("failed to dup fd: %v", err)
-		}
-
-		fmt.Printf("Re-executing proxy process\n")
-
-		args.Params[4] = strconv.Itoa(int(newFd))
-		execArgs := append([]string{"lxd", "forkproxy"}, args.Params...)
-
-		err = syscall.Exec(util.GetExecPath(), execArgs, os.Environ())
-		if err != nil {
-			return fmt.Errorf("failed to re-exec: %v", err)
-		}
-	}
-
-	// Re-create listener from fd
-	listenFile := os.NewFile(uintptr(fd), "listener")
-	listener, err := net.FileListener(listenFile)
-	if err != nil {
-		return fmt.Errorf("failed to re-assemble listener: %v", err)
-	}
-
-	defer listener.Close()
-
-	fmt.Printf("Starting to proxy\n")
-
-	// begin proxying
-	for {
-		// Accept a new client
-		srcConn, err := listener.Accept()
-		if err != nil {
-			fmt.Printf("error: Failed to accept new connection: %v\n", err)
-			continue
-		}
-		fmt.Printf("Accepted a new connection\n")
-
-		// Connect to the target
-		dstConn, err := getDestConn(connectAddr)
-		if err != nil {
-			fmt.Printf("error: Failed to connect to target: %v\n", err)
-			srcConn.Close()
-			continue
-		}
-
-		go io.Copy(srcConn, dstConn)
-		go io.Copy(dstConn, srcConn)
-	}
-}
-
-func getListenerFile(listenAddr string) (os.File, error) {
-	fields := strings.SplitN(listenAddr, ":", 2)
-	addr := strings.Join(fields[1:], "")
-
-	listener, err := net.Listen(fields[0], addr)
-	if err != nil {
-		return os.File{}, err
-	}
-
-	file := &os.File{}
-	switch listener.(type) {
-	case *net.TCPListener:
-		tcpListener := listener.(*net.TCPListener)
-		file, err = tcpListener.File()
-	case *net.UnixListener:
-		unixListener := listener.(*net.UnixListener)
-		file, err = unixListener.File()
-	}
-
-	if err != nil {
-		return os.File{}, fmt.Errorf("Failed to get file from listener: %v", err)
-	}
-
-	return *file, nil
-}
-
-func getDestConn(connectAddr string) (net.Conn, error) {
-	fields := strings.SplitN(connectAddr, ":", 2)
-	addr := strings.Join(fields[1:], "")
-	return net.Dial(fields[0], addr)
-}

From f125f9f1ce1dd0fb1c3c9045580ce2cc28b1f3c3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 14 Mar 2018 01:00:56 -0400
Subject: [PATCH 14/19] lxd/nsexec: Fix advance_arg and simplify code
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/main_forkfile.go  | 73 ++++++++++++++++++++-------------------------------
 lxd/main_forkmount.go | 37 +++++++++++++-------------
 lxd/main_forknet.go   | 15 +++++------
 lxd/main_forkproxy.go | 26 +++++++-----------
 lxd/main_nsexec.go    | 62 ++++++++++++++++++++++---------------------
 5 files changed, 96 insertions(+), 117 deletions(-)

diff --git a/lxd/main_forkfile.go b/lxd/main_forkfile.go
index eb6650295..78441df04 100644
--- a/lxd/main_forkfile.go
+++ b/lxd/main_forkfile.go
@@ -18,7 +18,7 @@ import (
 #include <sys/stat.h>
 #include <unistd.h>
 
-extern bool advance_arg(char *buf, char *cur, ssize_t size, bool required);
+extern char* advance_arg(bool required);
 extern void error(char *msg);
 extern void attach_userns(int pid);
 extern int dosetns(int pid, char *nstype);
@@ -267,7 +267,9 @@ close_host:
 	return ret;
 }
 
-void forkdofile(char *buf, char *cur, ssize_t size, bool is_put, char *rootfs, pid_t pid) {
+void forkdofile(bool is_put, char *rootfs, pid_t pid) {
+	char *cur = NULL;
+
 	uid_t uid = 0;
 	uid_t defaultUid = 0;
 
@@ -285,14 +287,14 @@ void forkdofile(char *buf, char *cur, ssize_t size, bool is_put, char *rootfs, p
 	bool append = false;
 
 
-	advance_arg(buf, cur, size, true);
+	cur = advance_arg(true);
 	if (is_put) {
 		source = cur;
 	} else {
 		target = cur;
 	}
 
-	advance_arg(buf, cur, size, true);
+	cur = advance_arg(true);
 	if (is_put) {
 		target = cur;
 	} else {
@@ -300,29 +302,15 @@ void forkdofile(char *buf, char *cur, ssize_t size, bool is_put, char *rootfs, p
 	}
 
 	if (is_put) {
-		advance_arg(buf, cur, size, true);
-		type = cur;
-
-		advance_arg(buf, cur, size, true);
-		uid = atoi(cur);
-
-		advance_arg(buf, cur, size, true);
-		gid = atoi(cur);
-
-		advance_arg(buf, cur, size, true);
-		mode = atoi(cur);
-
-		advance_arg(buf, cur, size, true);
-		defaultUid = atoi(cur);
-
-		advance_arg(buf, cur, size, true);
-		defaultGid = atoi(cur);
-
-		advance_arg(buf, cur, size, true);
-		defaultMode = atoi(cur);
-
-		advance_arg(buf, cur, size, true);
-		if (strcmp(cur, "append") == 0) {
+		type = advance_arg(true);
+		uid = atoi(advance_arg(true));
+		gid = atoi(advance_arg(true));
+		mode = atoi(advance_arg(true));
+		defaultUid = atoi(advance_arg(true));
+		defaultGid = atoi(advance_arg(true));
+		defaultMode = atoi(advance_arg(true));
+
+		if (strcmp(advance_arg(true), "append") == 0) {
 			append = true;
 		}
 	}
@@ -332,11 +320,10 @@ void forkdofile(char *buf, char *cur, ssize_t size, bool is_put, char *rootfs, p
 	_exit(manip_file_in_ns(rootfs, pid, source, target, is_put, type, uid, gid, mode, defaultUid, defaultGid, defaultMode, append));
 }
 
-void forkcheckfile(char *buf, char *cur, ssize_t size, char *rootfs, pid_t pid) {
+void forkcheckfile(char *rootfs, pid_t pid) {
 	char *path = NULL;
 
-	advance_arg(buf, cur, size, true);
-	path = cur;
+	path = advance_arg(true);
 
 	if (pid > 0) {
 		attach_userns(pid);
@@ -365,12 +352,11 @@ void forkcheckfile(char *buf, char *cur, ssize_t size, char *rootfs, pid_t pid)
 	_exit(0);
 }
 
-void forkremovefile(char *buf, char *cur, ssize_t size, char *rootfs, pid_t pid) {
+void forkremovefile(char *rootfs, pid_t pid) {
 	char *path = NULL;
 	struct stat sb;
 
-	advance_arg(buf, cur, size, true);
-	path = cur;
+	path = advance_arg(true);
 
 	if (pid > 0) {
 		attach_userns(pid);
@@ -411,18 +397,18 @@ void forkremovefile(char *buf, char *cur, ssize_t size, char *rootfs, pid_t pid)
 	_exit(0);
 }
 
-void forkfile(char *buf, char *cur, ssize_t size) {
+void forkfile() {
+	char *cur = NULL;
+
 	char *command = NULL;
 	char *rootfs = NULL;
 	pid_t pid = 0;
 
 	// Get the subcommand
-	if (advance_arg(buf, cur, size, false)) {
-		command = cur;
-	}
+	command = advance_arg(false);
 
 	// Get the container rootfs
-	advance_arg(buf, cur, size, false);
+	cur = advance_arg(false);
 	if (command == NULL || (strcmp(cur, "--help") == 0 || strcmp(cur, "--version") == 0 || strcmp(cur, "-h") == 0)) {
 		return;
 	}
@@ -430,8 +416,7 @@ void forkfile(char *buf, char *cur, ssize_t size) {
 	rootfs = cur;
 
 	// Get the container PID
-	advance_arg(buf, cur, size, true);
-	pid = atoi(cur);
+	pid = atoi(advance_arg(true));
 
 	// Check that we're root
 	if (geteuid() != 0) {
@@ -441,13 +426,13 @@ void forkfile(char *buf, char *cur, ssize_t size) {
 
 	// Call the subcommands
 	if (strcmp(command, "push") == 0) {
-		forkdofile(buf, cur, size, true, rootfs, pid);
+		forkdofile(true, rootfs, pid);
 	} else if (strcmp(command, "pull") == 0) {
-		forkdofile(buf, cur, size, false, rootfs, pid);
+		forkdofile(false, rootfs, pid);
 	} else if (strcmp(command, "exists") == 0) {
-		forkcheckfile(buf, cur, size, rootfs, pid);
+		forkcheckfile(rootfs, pid);
 	} else if (strcmp(command, "remove") == 0) {
-		forkremovefile(buf, cur, size, rootfs, pid);
+		forkremovefile(rootfs, pid);
 	}
 }
 */
diff --git a/lxd/main_forkmount.go b/lxd/main_forkmount.go
index a2b14c944..5c02e5e75 100644
--- a/lxd/main_forkmount.go
+++ b/lxd/main_forkmount.go
@@ -20,7 +20,7 @@ import (
 #include <sys/types.h>
 #include <unistd.h>
 
-extern bool advance_arg(char *buf, char *cur, ssize_t size, bool required);
+extern char* advance_arg(bool required);
 extern void error(char *msg);
 extern void attach_userns(int pid);
 extern int dosetns(int pid, char *nstype);
@@ -118,7 +118,7 @@ void create(char *src, char *dest) {
 	}
 }
 
-void forkdomount(char *buf, char *cur, ssize_t size, pid_t pid) {
+void forkdomount(pid_t pid) {
 	char *src, *dest, *opts;
 
 	attach_userns(pid);
@@ -128,11 +128,8 @@ void forkdomount(char *buf, char *cur, ssize_t size, pid_t pid) {
 		_exit(1);
 	}
 
-	advance_arg(buf, cur, size, true);
-	src = cur;
-
-	advance_arg(buf, cur, size, true);
-	dest = cur;
+	src = advance_arg(true);
+	dest = advance_arg(true);
 
 	create(src, dest);
 
@@ -157,37 +154,39 @@ void forkdomount(char *buf, char *cur, ssize_t size, pid_t pid) {
 	_exit(0);
 }
 
-void forkdoumount(char *buf, char *cur, ssize_t size, pid_t pid) {
+void forkdoumount(pid_t pid) {
+	char *path = NULL;
+
 	if (dosetns(pid, "mnt") < 0) {
 		fprintf(stderr, "Failed setns to container mount namespace: %s\n", strerror(errno));
 		_exit(1);
 	}
 
-	advance_arg(buf, cur, size, true);
-	if (access(cur, F_OK) < 0) {
+	path = advance_arg(true);
+	if (access(path, F_OK) < 0) {
 		fprintf(stderr, "Mount path doesn't exist: %s\n", strerror(errno));
 		_exit(1);
 	}
 
-	if (umount2(cur, MNT_DETACH) < 0) {
-		fprintf(stderr, "Error unmounting %s: %s\n", cur, strerror(errno));
+	if (umount2(path, MNT_DETACH) < 0) {
+		fprintf(stderr, "Error unmounting %s: %s\n", path, strerror(errno));
 		_exit(1);
 	}
 	_exit(0);
 }
 
-void forkmount(char *buf, char *cur, ssize_t size) {
+void forkmount() {
+	char *cur = NULL;
+
 	char *command = NULL;
 	char *rootfs = NULL;
 	pid_t pid = 0;
 
 	// Get the subcommand
-	if (advance_arg(buf, cur, size, false)) {
-		command = cur;
-	}
+	command = advance_arg(false);
 
 	// Get the pid
-	advance_arg(buf, cur, size, false);
+	cur = advance_arg(false);
 	if (command == NULL || (strcmp(cur, "--help") == 0 || strcmp(cur, "--version") == 0 || strcmp(cur, "-h") == 0)) {
 		return;
 	}
@@ -202,9 +201,9 @@ void forkmount(char *buf, char *cur, ssize_t size) {
 
 	// Call the subcommands
 	if (strcmp(command, "mount") == 0) {
-		forkdomount(buf, cur, size, pid);
+		forkdomount(pid);
 	} else if (strcmp(command, "umount") == 0) {
-		forkdoumount(buf, cur, size, pid);
+		forkdoumount(pid);
 	}
 }
 */
diff --git a/lxd/main_forknet.go b/lxd/main_forknet.go
index cadd714fb..d6685d0e5 100644
--- a/lxd/main_forknet.go
+++ b/lxd/main_forknet.go
@@ -23,10 +23,10 @@ import (
 #include <sys/types.h>
 #include <unistd.h>
 
-extern bool advance_arg(char *buf, char *cur, ssize_t size, bool required);
+extern char *advance_arg(bool required);
 extern int dosetns(int pid, char *nstype);
 
-void forkdonetinfo(char *buf, char *cur, ssize_t size, pid_t pid) {
+void forkdonetinfo(pid_t pid) {
 	if (dosetns(pid, "net") < 0) {
 		fprintf(stderr, "Failed setns to container network namespace: %s\n", strerror(errno));
 		_exit(1);
@@ -35,17 +35,16 @@ void forkdonetinfo(char *buf, char *cur, ssize_t size, pid_t pid) {
 	// Jump back to Go for the rest
 }
 
-void forknet(char *buf, char *cur, ssize_t size) {
+void forknet() {
 	char *command = NULL;
+	char *cur = NULL;
 	pid_t pid = 0;
 
 	// Get the subcommand
-	if (advance_arg(buf, cur, size, false)) {
-		command = cur;
-	}
+	command = advance_arg(false);
 
 	// Get the pid
-	advance_arg(buf, cur, size, false);
+	cur = advance_arg(false);
 	if (command == NULL || (strcmp(cur, "--help") == 0 || strcmp(cur, "--version") == 0 || strcmp(cur, "-h") == 0)) {
 		return;
 	}
@@ -60,7 +59,7 @@ void forknet(char *buf, char *cur, ssize_t size) {
 
 	// Call the subcommands
 	if (strcmp(command, "info") == 0) {
-		forkdonetinfo(buf, cur, size, pid);
+		forkdonetinfo(pid);
 	}
 }
 */
diff --git a/lxd/main_forkproxy.go b/lxd/main_forkproxy.go
index 08b05a104..7ce46a91f 100644
--- a/lxd/main_forkproxy.go
+++ b/lxd/main_forkproxy.go
@@ -24,30 +24,24 @@ import (
 #include <string.h>
 #include <unistd.h>
 
-extern bool advance_arg(char *buf, char *cur, ssize_t size, bool required);
+extern char* advance_arg(bool required);
 extern int dosetns(int pid, char *nstype);
 
-void forkproxy(char *buf, char *cur, ssize_t size) {
+void forkproxy() {
 	int cmdline, listen_pid, connect_pid, fdnum, forked, childPid, ret;
 	char fdpath[80];
 	char *logPath = NULL, *pidPath = NULL;
 	FILE *logFile = NULL, *pidFile = NULL;
 
 	// Get the arguments
-	advance_arg(buf, cur, size, true);
-	listen_pid = atoi(cur);
-	advance_arg(buf, cur, size, true);
-	advance_arg(buf, cur, size, true);
-	connect_pid = atoi(cur);
-	advance_arg(buf, cur, size, true);
-	advance_arg(buf, cur, size, true);
-	fdnum = atoi(cur);
-	advance_arg(buf, cur, size, true);
-	forked = atoi(cur);
-	advance_arg(buf, cur, size, true);
-	logPath = cur;
-	advance_arg(buf, cur, size, true);
-	pidPath = cur;
+	listen_pid = atoi(advance_arg(true));
+	advance_arg(true);
+	connect_pid = atoi(advance_arg(true));
+	advance_arg(true);
+	fdnum = atoi(advance_arg(true));
+	forked = atoi(advance_arg(true));
+	logPath = advance_arg(true);
+	pidPath = advance_arg(true);
 
 	// Check if proxy daemon already forked
 	if (forked == 0) {
diff --git a/lxd/main_nsexec.go b/lxd/main_nsexec.go
index 3699bb6cd..ac465d020 100644
--- a/lxd/main_nsexec.go
+++ b/lxd/main_nsexec.go
@@ -31,27 +31,32 @@ package main
 #include <string.h>
 #include <unistd.h>
 
-#define CMDLINE_SIZE (8 * PATH_MAX)
+// External functions
+extern void forkfile();
+extern void forkmount();
+extern void forknet();
+extern void forkproxy();
 
-extern void forkfile(char *buf, char *cur, ssize_t size);
-extern void forkmount(char *buf, char *cur, ssize_t size);
-extern void forknet(char *buf, char *cur, ssize_t size);
-extern void forkproxy(char *buf, char *cur, ssize_t size);
+// Command line parsing and tracking
+#define CMDLINE_SIZE (8 * PATH_MAX)
+char cmdline_buf[CMDLINE_SIZE];
+char *cmdline_cur = NULL;
+ssize_t cmdline_size = -1;
 
-bool advance_arg(char *buf, char *cur, ssize_t size, bool required) {
-	while (*cur != 0)
-		cur++;
+char* advance_arg(bool required) {
+	while (*cmdline_cur != 0)
+		cmdline_cur++;
 
-	cur++;
-	if (size <= cur - buf) {
+	cmdline_cur++;
+	if (cmdline_size <= cmdline_cur - cmdline_buf) {
 		if (!required)
-			return false;
+			return NULL;
 
 		fprintf(stderr, "not enough arguments\n");
 		_exit(1);
 	}
 
-	return true;
+	return cmdline_cur;
 }
 
 void error(char *msg)
@@ -133,9 +138,6 @@ void attach_userns(int pid) {
 
 __attribute__((constructor)) void init(void) {
 	int cmdline;
-	char buf[CMDLINE_SIZE];
-	ssize_t size;
-	char *cur;
 
 	// Extract arguments
 	cmdline = open("/proc/self/cmdline", O_RDONLY);
@@ -144,8 +146,8 @@ __attribute__((constructor)) void init(void) {
 		_exit(232);
 	}
 
-	memset(buf, 0, sizeof(buf));
-	if ((size = read(cmdline, buf, sizeof(buf)-1)) < 0) {
+	memset(cmdline_buf, 0, sizeof(cmdline_buf));
+	if ((cmdline_size = read(cmdline, cmdline_buf, sizeof(cmdline_buf)-1)) < 0) {
 		close(cmdline);
 		error("error: read");
 		_exit(232);
@@ -153,22 +155,22 @@ __attribute__((constructor)) void init(void) {
 	close(cmdline);
 
 	// Skip the first argument (but don't fail on missing second argument)
-	cur = buf;
-	while (*cur != 0)
-		cur++;
-	cur++;
-	if (size <= cur - buf)
+	cmdline_cur = cmdline_buf;
+	while (*cmdline_cur != 0)
+		cmdline_cur++;
+	cmdline_cur++;
+	if (cmdline_size <= cmdline_cur - cmdline_buf)
 		return;
 
 	// Intercepts some subcommands
-	if (strcmp(cur, "forkfile") == 0) {
-		forkfile(buf, cur, size);
-	} else if (strcmp(cur, "forkmount") == 0) {
-		forkmount(buf, cur, size);
-	} else if (strcmp(cur, "forknet") == 0) {
-		forknet(buf, cur, size);
-	} else if (strcmp(cur, "forkproxy") == 0) {
-		forkproxy(buf, cur, size);
+	if (strcmp(cmdline_cur, "forkfile") == 0) {
+		forkfile();
+	} else if (strcmp(cmdline_cur, "forkmount") == 0) {
+		forkmount();
+	} else if (strcmp(cmdline_cur, "forknet") == 0) {
+		forknet();
+	} else if (strcmp(cmdline_cur, "forkproxy") == 0) {
+		forkproxy();
 	}
 }
 */

From 5866c83c92a23aa2a642a578c758f8e2dd2ddedc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 14 Mar 2018 12:44:15 -0400
Subject: [PATCH 15/19] lxd: Fix subcommand help
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/main.go           |  4 ++++
 lxd/main_forkfile.go  | 11 +++++------
 lxd/main_forkmount.go |  6 ++++--
 lxd/main_forknet.go   |  7 +++++--
 lxd/main_forkproxy.go | 10 +++++++++-
 5 files changed, 27 insertions(+), 11 deletions(-)

diff --git a/lxd/main.go b/lxd/main.go
index 967e7c772..47992d439 100644
--- a/lxd/main.go
+++ b/lxd/main.go
@@ -100,6 +100,10 @@ func main() {
 	forkmigrateCmd := cmdForkmigrate{global: &globalCmd}
 	app.AddCommand(forkmigrateCmd.Command())
 
+	// forkmount sub-command
+	forkmountCmd := cmdForkmount{global: &globalCmd}
+	app.AddCommand(forkmountCmd.Command())
+
 	// forknet sub-command
 	forknetCmd := cmdForknet{global: &globalCmd}
 	app.AddCommand(forknetCmd.Command())
diff --git a/lxd/main_forkfile.go b/lxd/main_forkfile.go
index 78441df04..4c58ab223 100644
--- a/lxd/main_forkfile.go
+++ b/lxd/main_forkfile.go
@@ -398,23 +398,22 @@ void forkremovefile(char *rootfs, pid_t pid) {
 }
 
 void forkfile() {
-	char *cur = NULL;
-
 	char *command = NULL;
 	char *rootfs = NULL;
 	pid_t pid = 0;
 
 	// Get the subcommand
 	command = advance_arg(false);
+	if (command == NULL || (strcmp(command, "--help") == 0 || strcmp(command, "--version") == 0 || strcmp(command, "-h") == 0)) {
+		return;
+	}
 
 	// Get the container rootfs
-	cur = advance_arg(false);
-	if (command == NULL || (strcmp(cur, "--help") == 0 || strcmp(cur, "--version") == 0 || strcmp(cur, "-h") == 0)) {
+	rootfs = advance_arg(false);
+	if (rootfs == NULL || (strcmp(rootfs, "--help") == 0 || strcmp(rootfs, "--version") == 0 || strcmp(rootfs, "-h") == 0)) {
 		return;
 	}
 
-	rootfs = cur;
-
 	// Get the container PID
 	pid = atoi(advance_arg(true));
 
diff --git a/lxd/main_forkmount.go b/lxd/main_forkmount.go
index 5c02e5e75..b991c77ad 100644
--- a/lxd/main_forkmount.go
+++ b/lxd/main_forkmount.go
@@ -184,13 +184,15 @@ void forkmount() {
 
 	// Get the subcommand
 	command = advance_arg(false);
+	if (command == NULL || (strcmp(command, "--help") == 0 || strcmp(command, "--version") == 0 || strcmp(command, "-h") == 0)) {
+		return;
+	}
 
 	// Get the pid
 	cur = advance_arg(false);
-	if (command == NULL || (strcmp(cur, "--help") == 0 || strcmp(cur, "--version") == 0 || strcmp(cur, "-h") == 0)) {
+	if (cur == NULL || (strcmp(cur, "--help") == 0 || strcmp(cur, "--version") == 0 || strcmp(cur, "-h") == 0)) {
 		return;
 	}
-
 	pid = atoi(cur);
 
 	// Check that we're root
diff --git a/lxd/main_forknet.go b/lxd/main_forknet.go
index d6685d0e5..39cbe0a18 100644
--- a/lxd/main_forknet.go
+++ b/lxd/main_forknet.go
@@ -40,15 +40,18 @@ void forknet() {
 	char *cur = NULL;
 	pid_t pid = 0;
 
+
 	// Get the subcommand
 	command = advance_arg(false);
+	if (command == NULL || (strcmp(command, "--help") == 0 || strcmp(command, "--version") == 0 || strcmp(command, "-h") == 0)) {
+		return;
+	}
 
 	// Get the pid
 	cur = advance_arg(false);
-	if (command == NULL || (strcmp(cur, "--help") == 0 || strcmp(cur, "--version") == 0 || strcmp(cur, "-h") == 0)) {
+	if (cur == NULL || (strcmp(cur, "--help") == 0 || strcmp(cur, "--version") == 0 || strcmp(cur, "-h") == 0)) {
 		return;
 	}
-
 	pid = atoi(cur);
 
 	// Check that we're root
diff --git a/lxd/main_forkproxy.go b/lxd/main_forkproxy.go
index 7ce46a91f..d147aa494 100644
--- a/lxd/main_forkproxy.go
+++ b/lxd/main_forkproxy.go
@@ -28,13 +28,21 @@ extern char* advance_arg(bool required);
 extern int dosetns(int pid, char *nstype);
 
 void forkproxy() {
+	char *cur = NULL;
+
 	int cmdline, listen_pid, connect_pid, fdnum, forked, childPid, ret;
 	char fdpath[80];
 	char *logPath = NULL, *pidPath = NULL;
 	FILE *logFile = NULL, *pidFile = NULL;
 
+	// Get the pid
+	cur = advance_arg(false);
+	if (cur == NULL || (strcmp(cur, "--help") == 0 || strcmp(cur, "--version") == 0 || strcmp(cur, "-h") == 0)) {
+		return;
+	}
+	listen_pid = atoi(cur);
+
 	// Get the arguments
-	listen_pid = atoi(advance_arg(true));
 	advance_arg(true);
 	connect_pid = atoi(advance_arg(true));
 	advance_arg(true);

From 8efafa0c5d549b93b3a092df4c6c8c12fad4387e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Thu, 15 Mar 2018 16:41:58 -0400
Subject: [PATCH 16/19] lxd: Remove dead code
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/main_subcommand.go      | 103 --------------------------------------------
 lxd/main_subcommand_test.go |  91 --------------------------------------
 2 files changed, 194 deletions(-)
 delete mode 100644 lxd/main_subcommand.go
 delete mode 100644 lxd/main_subcommand_test.go

diff --git a/lxd/main_subcommand.go b/lxd/main_subcommand.go
deleted file mode 100644
index ed5575be2..000000000
--- a/lxd/main_subcommand.go
+++ /dev/null
@@ -1,103 +0,0 @@
-package main
-
-import (
-	"fmt"
-
-	log "github.com/lxc/lxd/shared/log15"
-
-	"github.com/lxc/lxd/shared"
-	"github.com/lxc/lxd/shared/cmd"
-	"github.com/lxc/lxd/shared/logger"
-	"github.com/lxc/lxd/shared/logging"
-	"github.com/lxc/lxd/shared/version"
-)
-
-// SubCommand is function that performs the logic of a specific LXD sub-command.
-type SubCommand func(*Args) error
-
-// SubCommandError implements the error interface and also carries with it an integer
-// exit code. If a Command returns an error of this kind, it will use its code
-// as exit status.
-type SubCommandError struct {
-	Code    int
-	Message string
-}
-
-func (e *SubCommandError) Error() string {
-	return e.Message
-}
-
-// SubCommandErrorf returns a new SubCommandError with the given code and the
-// given message (formatted with fmt.Sprintf).
-func SubCommandErrorf(code int, format string, a ...interface{}) *SubCommandError {
-	return &SubCommandError{
-		Code:    code,
-		Message: fmt.Sprintf(format, a...),
-	}
-}
-
-// RunSubCommand is the main entry point for all LXD subcommands, performing
-// common setup logic before firing up the subcommand.
-//
-// The ctx parameter provides input/output streams and related utilities, the
-// args one contains command line parameters, and handler is an additional
-// custom handler which will be added to the configured logger, along with the
-// default one (stderr) and the ones possibly installed by command line
-// arguments (via args.Syslog and args.Logfile).
-func RunSubCommand(command SubCommand, ctx *cmd.Context, args *Args, handler log.Handler) int {
-	// In case of --help or --version we just print the relevant output and
-	// return immediately
-	if args.Help {
-		ctx.Output(usage)
-		return 0
-	}
-	if args.Version {
-		ctx.Output("%s\n", version.Version)
-		return 0
-	}
-
-	// Run the setup code and, if successful, the command.
-	err := setupSubCommand(ctx, args, handler)
-	if err == nil {
-		err = command(args)
-	}
-	if err != nil {
-		code := 1
-		message := err.Error()
-		subCommandError, ok := err.(*SubCommandError)
-		if ok {
-			code = subCommandError.Code
-		}
-		if message != "" {
-			// FIXME: with Go 1.6, go vet complains if we just write
-			//        this as ctx.Error("error: %s\n", message), while
-			//        with Go > 1.6 it'd be fine.
-			ctx.Error(fmt.Sprintf("error: %s\n", message))
-		}
-		return code
-	}
-	return 0
-}
-
-// Setup logic common across all LXD subcommands.
-func setupSubCommand(context *cmd.Context, args *Args, handler log.Handler) error {
-	// Check if LXD_DIR is valid.
-	if len(shared.VarPath("unix.sock")) > 107 {
-		return fmt.Errorf("LXD_DIR is too long, must be < %d", 107-len("unix.sock"))
-	}
-
-	// Configure logging.
-	syslog := ""
-	if args.Syslog {
-		syslog = "lxd"
-	}
-
-	var err error
-	logger.Log, err = logging.GetLogger(syslog, args.Logfile, args.Verbose, args.Debug, handler)
-	if err != nil {
-		context.Output("%v\n", err)
-		return err
-	}
-
-	return nil
-}
diff --git a/lxd/main_subcommand_test.go b/lxd/main_subcommand_test.go
deleted file mode 100644
index 561325751..000000000
--- a/lxd/main_subcommand_test.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"os"
-	"strings"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-
-	"github.com/lxc/lxd/shared/cmd"
-	"github.com/lxc/lxd/shared/version"
-)
-
-// If the help flag is set in the command line, the usage message is printed
-// and the runner exists without executing the command.
-func TestRunSubCommand_Help(t *testing.T) {
-	command := newFailingCommand(t)
-	ctx, streams := newSubCommandContext()
-	args := &Args{Help: true}
-
-	assert.Equal(t, 0, RunSubCommand(command, ctx, args, nil))
-	assert.Contains(t, streams.Out(), "Usage: lxd [command] [options]")
-}
-
-// If the version flag is set in the command line, the version is printed
-// and the runner exists without executing the command.
-func TestRunSubCommand_Version(t *testing.T) {
-	command := newFailingCommand(t)
-	ctx, streams := newSubCommandContext()
-	args := &Args{Version: true}
-
-	assert.Equal(t, 0, RunSubCommand(command, ctx, args, nil))
-	assert.Contains(t, streams.Out(), version.Version)
-}
-
-// If the path set in LXD_DIR is too long, an error is printed.
-func TestRunSubCommand_LxdDirTooLong(t *testing.T) {
-	// Restore original LXD_DIR.
-	if value, ok := os.LookupEnv("LXD_DIR"); ok {
-		defer os.Setenv("LXD_DIR", value)
-	} else {
-		defer os.Unsetenv("LXD_DIR")
-	}
-
-	os.Setenv("LXD_DIR", strings.Repeat("x", 200))
-
-	command := newFailingCommand(t)
-	ctx, streams := newSubCommandContext()
-	args := &Args{}
-
-	assert.Equal(t, 1, RunSubCommand(command, ctx, args, nil))
-	assert.Contains(t, streams.Err(), "error: LXD_DIR is too long")
-}
-
-// If the command being executed returns an error, it is printed on standard
-// err.
-func TestRunSubCommand_Error(t *testing.T) {
-	command := func(*Args) error { return fmt.Errorf("boom") }
-	ctx, streams := newSubCommandContext()
-	args := &Args{}
-
-	assert.Equal(t, 1, RunSubCommand(command, ctx, args, nil))
-	assert.Equal(t, "error: boom\n", streams.Err())
-}
-
-// If the command being executed returns a SubCommandError, RunSubCommand
-// returns the relevant status code.
-func TestRunSubCommand_SubCommandError(t *testing.T) {
-	command := func(*Args) error { return SubCommandErrorf(127, "") }
-	ctx, streams := newSubCommandContext()
-	args := &Args{}
-
-	assert.Equal(t, 127, RunSubCommand(command, ctx, args, nil))
-	assert.Equal(t, "", streams.Err())
-}
-
-// Create a new cmd.Context connected to in-memory input/output streams.
-func newSubCommandContext() (*cmd.Context, *cmd.MemoryStreams) {
-	streams := cmd.NewMemoryStreams("")
-	context := cmd.NewMemoryContext(streams)
-	return context, streams
-}
-
-// Return a command that makes the test fail if executed.
-func newFailingCommand(t *testing.T) SubCommand {
-	return func(*Args) error {
-		t.Fatal("unexpected command execution")
-		return nil
-	}
-}

From 246301fec47509f46e06d5d9b3a05e3d0768cec8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Fri, 16 Mar 2018 00:19:41 -0400
Subject: [PATCH 17/19] lxd: Add relevant root checks

---
 lxd/main_callhook.go           | 6 ++++++
 lxd/main_forkconsole.go        | 6 ++++++
 lxd/main_forkexec.go           | 6 ++++++
 lxd/main_forkmigrate.go        | 6 ++++++
 lxd/main_forkproxy.go          | 6 ++++++
 lxd/main_forkstart.go          | 6 ++++++
 lxd/main_import.go             | 7 +++++++
 lxd/main_migratedumpsuccess.go | 7 +++++++
 lxd/main_netcat.go             | 6 ++++++
 9 files changed, 56 insertions(+)

diff --git a/lxd/main_callhook.go b/lxd/main_callhook.go
index c02a3e747..1a7c8f58a 100644
--- a/lxd/main_callhook.go
+++ b/lxd/main_callhook.go
@@ -34,6 +34,7 @@ func (c *cmdCallhook) Command() *cobra.Command {
 }
 
 func (c *cmdCallhook) Run(cmd *cobra.Command, args []string) error {
+	// Sanity checks
 	if len(args) < 2 {
 		cmd.Help()
 
@@ -49,6 +50,11 @@ func (c *cmdCallhook) Run(cmd *cobra.Command, args []string) error {
 	state := args[2]
 	target := ""
 
+	// Only root should run this
+	if os.Geteuid() != 0 {
+		return fmt.Errorf("This must be run as root")
+	}
+
 	// Connect to LXD
 	d, err := lxd.ConnectLXDUnix(fmt.Sprintf("%s/unix.socket", path), nil)
 	if err != nil {
diff --git a/lxd/main_forkconsole.go b/lxd/main_forkconsole.go
index 12ede5920..d6ceb747a 100644
--- a/lxd/main_forkconsole.go
+++ b/lxd/main_forkconsole.go
@@ -34,6 +34,7 @@ func (c *cmdForkconsole) Command() *cobra.Command {
 }
 
 func (c *cmdForkconsole) Run(cmd *cobra.Command, args []string) error {
+	// Sanity checks
 	if len(args) != 5 {
 		cmd.Help()
 
@@ -44,6 +45,11 @@ func (c *cmdForkconsole) Run(cmd *cobra.Command, args []string) error {
 		return fmt.Errorf("Missing required arguments")
 	}
 
+	// Only root should run this
+	if os.Geteuid() != 0 {
+		return fmt.Errorf("This must be run as root")
+	}
+
 	name := args[0]
 	lxcpath := args[1]
 	configPath := args[2]
diff --git a/lxd/main_forkexec.go b/lxd/main_forkexec.go
index 455348713..2768a51c9 100644
--- a/lxd/main_forkexec.go
+++ b/lxd/main_forkexec.go
@@ -37,6 +37,7 @@ func (c *cmdForkexec) Command() *cobra.Command {
 }
 
 func (c *cmdForkexec) Run(cmd *cobra.Command, args []string) error {
+	// Sanity checks
 	if len(args) < 4 {
 		cmd.Help()
 
@@ -47,6 +48,11 @@ func (c *cmdForkexec) Run(cmd *cobra.Command, args []string) error {
 		return fmt.Errorf("Missing required arguments")
 	}
 
+	// Only root should run this
+	if os.Geteuid() != 0 {
+		return fmt.Errorf("This must be run as root")
+	}
+
 	name := args[0]
 	lxcpath := args[1]
 	configPath := args[2]
diff --git a/lxd/main_forkmigrate.go b/lxd/main_forkmigrate.go
index a812337ce..6ff9b8b52 100644
--- a/lxd/main_forkmigrate.go
+++ b/lxd/main_forkmigrate.go
@@ -34,6 +34,7 @@ func (c *cmdForkmigrate) Command() *cobra.Command {
 }
 
 func (c *cmdForkmigrate) Run(cmd *cobra.Command, args []string) error {
+	// Sanity checks
 	if len(args) != 5 {
 		cmd.Help()
 
@@ -44,6 +45,11 @@ func (c *cmdForkmigrate) Run(cmd *cobra.Command, args []string) error {
 		return fmt.Errorf("Missing required arguments")
 	}
 
+	// Only root should run this
+	if os.Geteuid() != 0 {
+		return fmt.Errorf("This must be run as root")
+	}
+
 	name := args[0]
 	lxcpath := args[1]
 	configPath := args[2]
diff --git a/lxd/main_forkproxy.go b/lxd/main_forkproxy.go
index d147aa494..eaba0f502 100644
--- a/lxd/main_forkproxy.go
+++ b/lxd/main_forkproxy.go
@@ -146,6 +146,7 @@ func (c *cmdForkproxy) Command() *cobra.Command {
 }
 
 func (c *cmdForkproxy) Run(cmd *cobra.Command, args []string) error {
+	// Sanity checks
 	if len(args) != 8 {
 		cmd.Help()
 
@@ -156,6 +157,11 @@ func (c *cmdForkproxy) Run(cmd *cobra.Command, args []string) error {
 		return fmt.Errorf("Missing required arguments")
 	}
 
+	// Only root should run this
+	if os.Geteuid() != 0 {
+		return fmt.Errorf("This must be run as root")
+	}
+
 	// Get all our arguments
 	listenPid := args[0]
 	listenAddr := args[1]
diff --git a/lxd/main_forkstart.go b/lxd/main_forkstart.go
index 55fc1d969..b94eff815 100644
--- a/lxd/main_forkstart.go
+++ b/lxd/main_forkstart.go
@@ -35,6 +35,7 @@ func (c *cmdForkstart) Command() *cobra.Command {
 }
 
 func (c *cmdForkstart) Run(cmd *cobra.Command, args []string) error {
+	// Sanity checks
 	if len(args) != 3 {
 		cmd.Help()
 
@@ -45,6 +46,11 @@ func (c *cmdForkstart) Run(cmd *cobra.Command, args []string) error {
 		return fmt.Errorf("Missing required arguments")
 	}
 
+	// Only root should run this
+	if os.Geteuid() != 0 {
+		return fmt.Errorf("This must be run as root")
+	}
+
 	name := args[0]
 	lxcpath := args[1]
 	configPath := args[2]
diff --git a/lxd/main_import.go b/lxd/main_import.go
index 1abfd94ab..829b3f492 100644
--- a/lxd/main_import.go
+++ b/lxd/main_import.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"fmt"
+	"os"
 
 	"github.com/spf13/cobra"
 
@@ -38,6 +39,7 @@ func (c *cmdImport) Command() *cobra.Command {
 }
 
 func (c *cmdImport) Run(cmd *cobra.Command, args []string) error {
+	// Sanity checks
 	if len(args) < 1 {
 		cmd.Help()
 
@@ -48,6 +50,11 @@ func (c *cmdImport) Run(cmd *cobra.Command, args []string) error {
 		return fmt.Errorf("Missing required arguments")
 	}
 
+	// Only root should run this
+	if os.Geteuid() != 0 {
+		return fmt.Errorf("This must be run as root")
+	}
+
 	name := args[0]
 	req := map[string]interface{}{
 		"name":  name,
diff --git a/lxd/main_migratedumpsuccess.go b/lxd/main_migratedumpsuccess.go
index 683a46a9f..65cf25e93 100644
--- a/lxd/main_migratedumpsuccess.go
+++ b/lxd/main_migratedumpsuccess.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"fmt"
+	"os"
 	"strings"
 
 	"github.com/spf13/cobra"
@@ -33,6 +34,7 @@ func (c *cmdMigratedumpsuccess) Command() *cobra.Command {
 }
 
 func (c *cmdMigratedumpsuccess) Run(cmd *cobra.Command, args []string) error {
+	// Sanity checks
 	if len(args) < 2 {
 		cmd.Help()
 
@@ -43,6 +45,11 @@ func (c *cmdMigratedumpsuccess) Run(cmd *cobra.Command, args []string) error {
 		return fmt.Errorf("Missing required arguments")
 	}
 
+	// Only root should run this
+	if os.Geteuid() != 0 {
+		return fmt.Errorf("This must be run as root")
+	}
+
 	d, err := lxd.ConnectLXDUnix("", nil)
 	if err != nil {
 		return err
diff --git a/lxd/main_netcat.go b/lxd/main_netcat.go
index 0d71f3d6e..85ed5c681 100644
--- a/lxd/main_netcat.go
+++ b/lxd/main_netcat.go
@@ -39,6 +39,7 @@ func (c *cmdNetcat) Command() *cobra.Command {
 }
 
 func (c *cmdNetcat) Run(cmd *cobra.Command, args []string) error {
+	// Sanity checks
 	if len(args) < 2 {
 		cmd.Help()
 
@@ -49,6 +50,11 @@ func (c *cmdNetcat) Run(cmd *cobra.Command, args []string) error {
 		return fmt.Errorf("Missing required arguments")
 	}
 
+	// Only root should run this
+	if os.Geteuid() != 0 {
+		return fmt.Errorf("This must be run as root")
+	}
+
 	logPath := shared.LogPath(args[1], "netcat.log")
 	if shared.PathExists(logPath) {
 		os.Remove(logPath)

From aae9189be53b70ea9f3974de4a4aafb429209647 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Thu, 15 Mar 2018 18:29:14 -0400
Subject: [PATCH 18/19] lxd: Port init to cobra
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 doc/clustering.md               |    2 +-
 lxd/main.go                     |    4 +
 lxd/main_init.go                | 1475 ++++++++++-----------------------------
 lxd/main_init_auto.go           |  114 +++
 lxd/main_init_interactive.go    |  490 +++++++++++++
 lxd/main_init_preseed.go        |   29 +
 lxd/main_init_test.go           |  824 ----------------------
 shared/cmd/ask.go               |  141 ++++
 shared/cmd/context.go           |  181 -----
 shared/cmd/context_test.go      |  195 ------
 shared/cmd/doc.go               |   11 -
 shared/cmd/parser.go            |  142 ----
 shared/cmd/parser_test.go       |  162 -----
 shared/cmd/testing.go           |   77 --
 test/includes/clustering.sh     |    2 +-
 test/suites/init_interactive.sh |    5 +-
 16 files changed, 1141 insertions(+), 2713 deletions(-)
 create mode 100644 lxd/main_init_auto.go
 create mode 100644 lxd/main_init_interactive.go
 create mode 100644 lxd/main_init_preseed.go
 delete mode 100644 lxd/main_init_test.go
 create mode 100644 shared/cmd/ask.go
 delete mode 100644 shared/cmd/context.go
 delete mode 100644 shared/cmd/context_test.go
 delete mode 100644 shared/cmd/doc.go
 delete mode 100644 shared/cmd/parser.go
 delete mode 100644 shared/cmd/parser_test.go
 delete mode 100644 shared/cmd/testing.go

diff --git a/doc/clustering.md b/doc/clustering.md
index 58b5f32f4..bfe5c0c17 100644
--- a/doc/clustering.md
+++ b/doc/clustering.md
@@ -133,7 +133,7 @@ opyQ1VRpAg2sV2C4W8irbNqeUsTeZZxhLqp4vNOXXBBrSqUCdPu1JXADV0kavg1l
 
 -----END CERTIFICATE-----
 "
-cluster_password: sekret
+  cluster_password: sekret
 ```
 
 ## Managing a cluster
diff --git a/lxd/main.go b/lxd/main.go
index 47992d439..7a3be32d5 100644
--- a/lxd/main.go
+++ b/lxd/main.go
@@ -120,6 +120,10 @@ func main() {
 	importCmd := cmdImport{global: &globalCmd}
 	app.AddCommand(importCmd.Command())
 
+	// init sub-command
+	initCmd := cmdInit{global: &globalCmd}
+	app.AddCommand(initCmd.Command())
+
 	// migratedumpsuccess sub-command
 	migratedumpsuccessCmd := cmdMigratedumpsuccess{global: &globalCmd}
 	app.AddCommand(migratedumpsuccessCmd.Command())
diff --git a/lxd/main_init.go b/lxd/main_init.go
index 3c043e77a..534bd09f2 100644
--- a/lxd/main_init.go
+++ b/lxd/main_init.go
@@ -1,1266 +1,507 @@
 package main
 
 import (
-	"encoding/pem"
 	"fmt"
-	"net"
-	"os"
-	"os/exec"
-	"path/filepath"
-	"strconv"
-	"strings"
-	"syscall"
 
-	"golang.org/x/crypto/ssh/terminal"
+	"github.com/pkg/errors"
+	"github.com/spf13/cobra"
 
 	"github.com/lxc/lxd/client"
 	"github.com/lxc/lxd/lxd/cluster"
 	"github.com/lxc/lxd/lxd/util"
-	"github.com/pkg/errors"
-
 	"github.com/lxc/lxd/shared"
 	"github.com/lxc/lxd/shared/api"
-	"github.com/lxc/lxd/shared/cmd"
-	"github.com/lxc/lxd/shared/idmap"
-	"github.com/lxc/lxd/shared/logger"
 )
 
-// CmdInit implements the "lxd init" command line.
-type CmdInit struct {
-	Context         *cmd.Context
-	Args            *Args
-	RunningInUserns bool
-	VarDir          string
-	PasswordReader  func(int) ([]byte, error)
+type initData struct {
+	api.ServerPut `yaml:",inline"`
+	Cluster       *initDataCluster       `json:"cluster" yaml:"cluster"`
+	Networks      []api.NetworksPost     `json:"networks" yaml:"networks"`
+	StoragePools  []api.StoragePoolsPost `json:"storage_pools" yaml:"storage_pools"`
+	Profiles      []api.ProfilesPost     `json:"profiles" yaml:"profiles"`
 }
 
-// Run triggers the execution of the init command.
-func (cmd *CmdInit) Run() error {
-	// Check that command line arguments don't conflict with each other
-	err := cmd.validateArgs()
-	if err != nil {
-		return err
-	}
+type initDataCluster struct {
+	api.ClusterPut  `yaml:",inline"`
+	ClusterPassword string `json:"cluster_password" yaml:"cluster_password"`
+}
 
-	// Connect to LXD
-	path := ""
-	if cmd.VarDir != "" {
-		path = filepath.Join(cmd.VarDir, "unix.socket")
-	}
-	client, err := lxd.ConnectLXDUnix(path, nil)
-	if err != nil {
-		return fmt.Errorf("Unable to talk to LXD: %s", err)
-	}
+type cmdInit struct {
+	cmd    *cobra.Command
+	global *cmdGlobal
 
-	existingPools, err := client.GetStoragePoolNames()
-	if err != nil {
-		// We should consider this fatal since this means
-		// something's wrong with the daemon.
-		return err
-	}
+	flagAuto    bool
+	flagPreseed bool
 
-	data := &cmdInitData{}
+	flagNetworkAddress  string
+	flagNetworkPort     int
+	flagStorageBackend  string
+	flagStorageDevice   string
+	flagStorageLoopSize int
+	flagStoragePool     string
+	flagTrustPassword   string
+}
 
-	// Kick off the appropriate way to fill the data (either
-	// preseed, auto or interactive).
-	if cmd.Args.Preseed {
-		err = cmd.fillDataPreseed(data, client)
-	} else {
-		// Copy the data from the current default profile, if it exists.
-		cmd.fillDataWithCurrentServerConfig(data, client)
+func (c *cmdInit) Command() *cobra.Command {
+	cmd := &cobra.Command{}
+	cmd.Use = "init"
+	cmd.Short = "Configure the LXD daemon"
+	cmd.Long = `Description:
+  Configure the LXD daemon
+`
+	cmd.Example = `  init --preseed
+  init --auto [--network-address=IP] [--network-port=8443] [--storage-backend=dir]
+              [--storage-create-device=DEVICE] [--storage-create-loop=SIZE]
+              [--storage-pool=POOL] [--trust-password=PASSWORD]
+`
+	cmd.RunE = c.Run
+	cmd.Flags().BoolVar(&c.flagAuto, "auto", false, "Automatic (non-interactive) mode")
+	cmd.Flags().BoolVar(&c.flagPreseed, "preseed", false, "Pre-seed mode, expects YAML config from stdin")
 
-		// Copy the data from the current server config.
-		cmd.fillDataWithCurrentDefaultProfile(data, client)
+	cmd.Flags().StringVar(&c.flagNetworkAddress, "network-address", "", "Address to bind LXD to (default: none)"+"``")
+	cmd.Flags().IntVar(&c.flagNetworkPort, "network-port", -1, "Port to bind LXD to (default: 8443)"+"``")
+	cmd.Flags().StringVar(&c.flagStorageBackend, "storage-backend", "", "Storage backend to use (btrfs, dir, lvm or zfs, default: dir)"+"``")
+	cmd.Flags().StringVar(&c.flagStorageDevice, "storage-create-device", "", "Setup device based storage using DEVICE"+"``")
+	cmd.Flags().IntVar(&c.flagStorageLoopSize, "storage-create-loop", -1, "Setup loop based storage with SIZE in GB"+"``")
+	cmd.Flags().StringVar(&c.flagStoragePool, "storage-pool", "", "Storage pool to use or create"+"``")
+	cmd.Flags().StringVar(&c.flagTrustPassword, "trust-password", "", "Password required to add new clients"+"``")
 
-		// Figure what storage drivers among the supported ones are actually
-		// available on this system.
-		backendsAvailable := cmd.availableStoragePoolsDrivers()
+	c.cmd = cmd
+	return cmd
+}
 
-		if cmd.Args.Auto {
-			err = cmd.fillDataAuto(data, client, backendsAvailable, existingPools)
-		} else {
-			err = cmd.fillDataInteractive(data, client, backendsAvailable, existingPools)
-		}
-	}
-	if err != nil {
-		return err
+func (c *cmdInit) Run(cmd *cobra.Command, args []string) error {
+	// Sanity checks
+	if c.flagAuto && c.flagPreseed {
+		return fmt.Errorf("Can't use --auto and --preseed together")
 	}
 
-	// Apply the desired configuration.
-	err = cmd.apply(client, data)
-	if err != nil {
-		return err
+	if !c.flagAuto && (c.flagNetworkAddress != "" || c.flagNetworkPort != -1 ||
+		c.flagStorageBackend != "" || c.flagStorageDevice != "" ||
+		c.flagStorageLoopSize != -1 || c.flagStoragePool != "" ||
+		c.flagTrustPassword != "") {
+		return fmt.Errorf("Configuration flags require --auto")
 	}
 
-	cmd.Context.Output("LXD has been successfully configured.\n")
-
-	return nil
-}
-
-// Fill the given configuration data with parameters collected from
-// the --auto command line.
-func (cmd *CmdInit) fillDataAuto(data *cmdInitData, client lxd.ContainerServer, backendsAvailable []string, existingPools []string) error {
-	if cmd.Args.StorageBackend == "" {
-		cmd.Args.StorageBackend = "dir"
-	}
-	err := cmd.validateArgsAuto(backendsAvailable)
+	// Connect to LXD
+	d, err := lxd.ConnectLXDUnix("", nil)
 	if err != nil {
-		return err
+		return errors.Wrap(err, "Failed to connect to local LXD")
 	}
 
-	if cmd.Args.NetworkAddress != "" {
-		// If no port was provided, use the default one
-		if cmd.Args.NetworkPort == -1 {
-			cmd.Args.NetworkPort = 8443
-		}
-		networking := &cmdInitNetworkingParams{
-			Address:       cmd.Args.NetworkAddress,
-			Port:          cmd.Args.NetworkPort,
-			TrustPassword: cmd.Args.TrustPassword,
-		}
-		cmd.fillDataWithNetworking(data, networking)
-	}
+	// Prepare the input data
+	var config *initData
 
-	if len(existingPools) == 0 {
-		storage := &cmdInitStorageParams{
-			Backend:  cmd.Args.StorageBackend,
-			LoopSize: cmd.Args.StorageCreateLoop,
-			Device:   cmd.Args.StorageCreateDevice,
-			Dataset:  cmd.Args.StorageDataset,
-			Pool:     "default",
-		}
-		err = cmd.fillDataWithStorage(data, storage, existingPools)
+	// Preseed mode
+	if c.flagPreseed {
+		config, err = c.RunPreseed(cmd, args, d)
 		if err != nil {
 			return err
 		}
 	}
-	return nil
-}
-
-// Fill the given configuration data with parameters collected with
-// interactive questions.
-func (cmd *CmdInit) fillDataInteractive(data *cmdInitData, client lxd.ContainerServer, backendsAvailable []string, existingPools []string) error {
-	clustering, err := cmd.askClustering()
-	if err != nil {
-		return err
-	}
 
-	// Ask to create basic entities only if we are not joining an existing
-	// cluster.
-	var storage *cmdInitStorageParams
-	var defaultPrivileged int
-	var networking *cmdInitNetworkingParams
-	var imagesAutoUpdate bool
-	var bridge *cmdInitBridgeParams
-
-	if clustering == nil || clustering.TargetAddress == "" {
-		storage, err = cmd.askStorage(client, existingPools, backendsAvailable)
+	// Auto mode
+	if c.flagAuto {
+		config, err = c.RunAuto(cmd, args, d)
 		if err != nil {
 			return err
 		}
-		defaultPrivileged = cmd.askDefaultPrivileged()
-
-		// Ask about networking only if we skipped the clustering questions.
-		if clustering == nil {
-			networking = cmd.askNetworking()
-		}
-
-		imagesAutoUpdate = cmd.askImages()
-		bridge = cmd.askBridge(client)
 	}
-	if clustering != nil {
-		// Re-use the answers to the clustering questions.
-		networking = &cmdInitNetworkingParams{
-			Address:       clustering.Address,
-			Port:          clustering.Port,
-			TrustPassword: clustering.TrustPassword,
-		}
-		if clustering.TargetAddress != "" {
-			// Add the joining node's certificate the cluster trust pool
-			cert, err := util.LoadCert(cmd.VarDir)
-			if err != nil {
-				return err
-			}
-			err = cluster.SetupTrust(
-				string(cert.PublicKey()), clustering.TargetAddress,
-				string(clustering.TargetCert), clustering.TargetPassword)
-			if err != nil {
-				return errors.Wrap(err, "failed to add joining node's certificate to cluster")
-			}
-
-			// Client parameters to connect to the target cluster node.
-			args := &lxd.ConnectionArgs{
-				TLSClientCert: string(cert.PublicKey()),
-				TLSClientKey:  string(cert.PrivateKey()),
-				TLSServerCert: string(clustering.TargetCert),
-			}
-			url := fmt.Sprintf("https://%s", clustering.TargetAddress)
-			client, err := lxd.ConnectLXD(url, args)
-			if err != nil {
-				return err
-			}
 
-			// Get the pools and networks defined on the target cluster
-			targetPools, err := client.GetStoragePools()
-			if err != nil {
-				return errors.Wrap(err, "failed to get cluster storage pools")
-			}
-			targetNetworks, err := client.GetNetworks()
-			if err != nil {
-				return errors.Wrap(err, "failed to get cluster networks")
-			}
-
-			// Ask for node-specific pools and networks config keys.
-			data.Pools, err = cmd.askClusteringStoragePools(targetPools)
-			if err != nil {
-				return err
-			}
-			data.Networks, err = cmd.askClusteringNetworks(targetNetworks)
-			if err != nil {
-				return err
-			}
+	// Interactive mode
+	if !c.flagAuto && !c.flagPreseed {
+		config, err = c.RunInteractive(cmd, args, d)
+		if err != nil {
+			return err
 		}
 	}
 
-	_, err = exec.LookPath("dnsmasq")
-	if err != nil && bridge != nil {
-		return fmt.Errorf("LXD managed bridges require \"dnsmasq\". Install it and try again.")
-	}
-
-	cmd.fillDataWithClustering(data, clustering)
-
-	err = cmd.fillDataWithStorage(data, storage, existingPools)
-	if err != nil {
-		return err
-	}
-
-	err = cmd.fillDataWithDefaultPrivileged(data, defaultPrivileged)
-	if err != nil {
-		return err
-	}
-
-	cmd.fillDataWithNetworking(data, networking)
-
-	cmd.fillDataWithImages(data, imagesAutoUpdate)
-
-	err = cmd.fillDataWithBridge(data, bridge)
-	if err != nil {
-		return err
-	}
-
-	return nil
+	return c.ApplyConfig(cmd, args, d, *config)
 }
 
-// Fill the given configuration data from the preseed YAML text stream.
-func (cmd *CmdInit) fillDataPreseed(data *cmdInitData, client lxd.ContainerServer) error {
-	err := cmd.Context.InputYAML(data)
-	if err != nil {
-		return fmt.Errorf("Invalid preseed YAML content")
-	}
-
-	return nil
-}
+func (c *cmdInit) availableStorageDrivers() []string {
+	drivers := []string{"dir"}
 
-// Fill the given data with the current server configuration.
-func (cmd *CmdInit) fillDataWithCurrentServerConfig(data *cmdInitData, client lxd.ContainerServer) error {
-	server, _, err := client.GetServer()
+	backingFs, err := util.FilesystemDetect(shared.VarPath())
 	if err != nil {
-		return err
-	}
-	data.ServerPut = server.Writable()
-	return nil
-}
-
-// Fill the given data with the current default profile, if it exists.
-func (cmd *CmdInit) fillDataWithCurrentDefaultProfile(data *cmdInitData, client lxd.ContainerServer) {
-	defaultProfile, _, err := client.GetProfile("default")
-	if err == nil {
-		// Copy the default profile configuration (that we have
-		// possibly modified above).
-		data.Profiles = []api.ProfilesPost{{Name: "default"}}
-		data.Profiles[0].ProfilePut = defaultProfile.ProfilePut
-	}
-}
-
-// Fill the given init data with clustering details matching the given
-// clustering parameters.
-func (cmd *CmdInit) fillDataWithClustering(data *cmdInitData, clustering *cmdInitClusteringParams) {
-	if clustering == nil {
-		return
-	}
-	data.Cluster.ServerName = clustering.Name
-	data.Cluster.Enabled = true
-	data.Cluster.ClusterAddress = clustering.TargetAddress
-	data.Cluster.ClusterCertificate = string(clustering.TargetCert)
-	data.ClusterPassword = clustering.TargetPassword
-}
-
-// Fill the given init data with a new storage pool structure matching the
-// given storage parameters.
-func (cmd *CmdInit) fillDataWithStorage(data *cmdInitData, storage *cmdInitStorageParams, existingPools []string) error {
-	if storage == nil {
-		return nil
-	}
-
-	// Pool configuration
-	storagePoolConfig := map[string]string{}
-	if storage.Config != nil {
-		storagePoolConfig = storage.Config
+		backingFs = "dir"
 	}
 
-	if storage.Device != "" {
-		storagePoolConfig["source"] = storage.Device
-		if storage.Dataset != "" {
-			storage.Pool = storage.Dataset
-		}
-	} else if storage.LoopSize != -1 {
-		if storage.Dataset != "" {
-			storage.Pool = storage.Dataset
+	// Check available backends
+	for _, driver := range supportedStoragePoolDrivers {
+		if driver == "dir" {
+			continue
 		}
-	} else {
-		storagePoolConfig["source"] = storage.Dataset
-	}
-
-	if storage.LoopSize > 0 {
-		storagePoolConfig["size"] = strconv.FormatInt(storage.LoopSize, 10) + "GB"
-	}
-
-	// Create the requested storage pool.
-	storageStruct := api.StoragePoolsPost{
-		Name:   storage.Pool,
-		Driver: storage.Backend,
-	}
-	storageStruct.Config = storagePoolConfig
-
-	data.Pools = []api.StoragePoolsPost{storageStruct}
-
-	// When lxd init is rerun and there are already storage pools
-	// configured, do not try to set a root disk device in the
-	// default profile again. Let the user figure this out.
-	if len(existingPools) == 0 {
-		if len(data.Profiles) != 0 {
-			defaultProfile := data.Profiles[0]
-			foundRootDiskDevice := false
-			for k, v := range defaultProfile.Devices {
-				if v["path"] == "/" && v["source"] == "" {
-					foundRootDiskDevice = true
-
-					// Unconditionally overwrite because if the user ends up
-					// with a clean LXD but with a pool property key existing in
-					// the default profile it must be empty otherwise it would
-					// not have been possible to delete the storage pool in
-					// the first place.
-					defaultProfile.Devices[k]["pool"] = storage.Pool
-					logger.Debugf("Set pool property of existing root disk device \"%s\" in profile \"default\" to \"%s\".", storage.Pool)
-
-					break
-				}
-			}
 
-			if !foundRootDiskDevice {
-				err := cmd.profileDeviceAlreadyExists(&defaultProfile, "root")
-				if err != nil {
-					return err
-				}
-
-				defaultProfile.Devices["root"] = map[string]string{
-					"type": "disk",
-					"path": "/",
-					"pool": storage.Pool,
-				}
-			}
-		} else {
-			logger.Warnf("Did not find profile \"default\" so no default storage pool will be set. Manual intervention needed.")
+		// btrfs can work in user namespaces too. (If
+		// source=/some/path/on/btrfs is used.)
+		if shared.RunningInUserNS() && (backingFs != "btrfs" || driver != "btrfs") {
+			continue
 		}
-	}
-
-	return nil
-}
-
-// Fill the default profile in the given init data with options about whether
-// to run in privileged mode.
-func (cmd *CmdInit) fillDataWithDefaultPrivileged(data *cmdInitData, defaultPrivileged int) error {
-	if defaultPrivileged == -1 {
-		return nil
-	}
-	if len(data.Profiles) == 0 {
-		return fmt.Errorf("error: profile 'default' profile not found")
-	}
-	defaultProfile := data.Profiles[0]
-	if defaultPrivileged == 0 {
-		defaultProfile.Config["security.privileged"] = ""
-	} else if defaultPrivileged == 1 {
-		defaultProfile.Config["security.privileged"] = "true"
-	}
-	return nil
-}
 
-// Fill the given init data with server config details matching the
-// given networking parameters.
-func (cmd *CmdInit) fillDataWithNetworking(data *cmdInitData, networking *cmdInitNetworkingParams) {
-	if networking == nil {
-		return
-	}
-	data.Config["core.https_address"] = fmt.Sprintf("%s:%d", networking.Address, networking.Port)
-	if networking.TrustPassword != "" {
-		data.Config["core.trust_password"] = networking.TrustPassword
-	}
-}
-
-// Fill the given init data with server config details matching the
-// given images auto update choice.
-func (cmd *CmdInit) fillDataWithImages(data *cmdInitData, imagesAutoUpdate bool) {
-	if imagesAutoUpdate {
-		if val, ok := data.Config["images.auto_update_interval"]; ok && val == "0" {
-			data.Config["images.auto_update_interval"] = ""
+		// Initialize a core storage interface for the given driver.
+		_, err := storageCoreInit(driver)
+		if err != nil {
+			continue
 		}
-	} else {
-		data.Config["images.auto_update_interval"] = "0"
-	}
-}
-
-// Fill the given init data with a new bridge network device structure
-// matching the given storage parameters.
-func (cmd *CmdInit) fillDataWithBridge(data *cmdInitData, bridge *cmdInitBridgeParams) error {
-	if bridge == nil {
-		return nil
-	}
 
-	bridgeConfig := map[string]string{}
-	bridgeConfig["ipv4.address"] = bridge.IPv4
-	bridgeConfig["ipv6.address"] = bridge.IPv6
-
-	if bridge.IPv4Nat {
-		bridgeConfig["ipv4.nat"] = "true"
-	}
-
-	if bridge.IPv6Nat {
-		bridgeConfig["ipv6.nat"] = "true"
-	}
-
-	network := api.NetworksPost{
-		Name: bridge.Name}
-	network.Config = bridgeConfig
-	data.Networks = []api.NetworksPost{network}
-
-	if len(data.Profiles) == 0 {
-		return fmt.Errorf("error: profile 'default' profile not found")
-	}
-
-	// Attach the bridge as eth0 device of the default profile, if such
-	// device doesn't exists yet.
-	defaultProfile := data.Profiles[0]
-	err := cmd.profileDeviceAlreadyExists(&defaultProfile, "eth0")
-	if err != nil {
-		return err
-	}
-	defaultProfile.Devices["eth0"] = map[string]string{
-		"type":    "nic",
-		"nictype": "bridged",
-		"parent":  bridge.Name,
+		drivers = append(drivers, driver)
 	}
 
-	return nil
-
+	return drivers
 }
 
-// Apply the configuration specified in the given init data.
-func (cmd *CmdInit) apply(client lxd.ContainerServer, data *cmdInitData) error {
-	// Functions that should be invoked to revert back to initial
-	// state any change that was successfully applied, in case
-	// anything goes wrong after that change.
-	reverters := make([]reverter, 0)
-
-	// Functions to apply the desired changes.
-	changers := make([](func() (reverter, error)), 0)
-
-	// Server config changer
-	changers = append(changers, func() (reverter, error) {
-		return cmd.initConfig(client, data.Config)
-	})
-
-	// Storage pool changers
-	for i := range data.Pools {
-		pool := data.Pools[i] // Local variable for the closure
-		changers = append(changers, func() (reverter, error) {
-			return cmd.initPool(client, pool)
-		})
-	}
+func (c *cmdInit) ApplyConfig(cmd *cobra.Command, args []string, d lxd.ContainerServer, config initData) error {
+	// Handle reverts
+	revert := true
+	reverts := []func(){}
+	defer func() {
+		if !revert {
+			return
+		}
 
-	// Network changers
-	for i := range data.Networks {
-		network := data.Networks[i] // Local variable for the closure
-		changers = append(changers, func() (reverter, error) {
-			return cmd.initNetwork(client, network)
-		})
-	}
+		// Lets undo things in reverse order
+		for i := len(reverts) - 1; i >= 0; i-- {
+			reverts[i]()
+		}
+	}()
 
-	// Profile changers
-	for i := range data.Profiles {
-		profile := data.Profiles[i] // Local variable for the closure
-		changers = append(changers, func() (reverter, error) {
-			return cmd.initProfile(client, profile)
-		})
-	}
+	// Apply server configuration
+	if config.Config != nil && len(config.Config) > 0 {
+		// Get current config
+		currentServer, etag, err := d.GetServer()
+		if err != nil {
+			return errors.Wrap(err, "Failed to retrieve current server configuration")
+		}
 
-	// Cluster changers
-	if data.Cluster.ServerName != "" {
-		changers = append(changers, func() (reverter, error) {
-			return cmd.initCluster(client, data.Cluster, data.ClusterPassword)
+		// Setup reverter
+		reverts = append(reverts, func() {
+			d.UpdateServer(currentServer.Writable(), "")
 		})
-	}
 
-	// Apply all changes. If anything goes wrong at any iteration
-	// of the loop, we'll try to revert any change performed in
-	// earlier iterations.
-	for _, changer := range changers {
-		reverter, err := changer()
+		// Prepare the update
+		newServer := api.ServerPut{}
+		err = shared.DeepCopy(currentServer.Writable(), &newServer)
 		if err != nil {
-			cmd.revert(reverters)
-			return err
+			return errors.Wrap(err, "Failed to copy server configuration")
 		}
-		// Save the revert function for later.
-		reverters = append(reverters, reverter)
-	}
 
-	return nil
-}
+		for k, v := range config.Config {
+			newServer.Config[k] = fmt.Sprintf("%v", v)
+		}
 
-// Try to revert the state to what it was before running the "lxd init" command.
-func (cmd *CmdInit) revert(reverters []reverter) {
-	for _, reverter := range reverters {
-		err := reverter()
+		// Apply it
+		err = d.UpdateServer(newServer, etag)
 		if err != nil {
-			logger.Warnf("Reverting to pre-init state failed: %s", err)
-			break
+			return errors.Wrap(err, "Failed to update server configuration")
 		}
 	}
-}
-
-// Apply the server-level configuration in the given map.
-func (cmd *CmdInit) initConfig(client lxd.ContainerServer, config map[string]interface{}) (reverter, error) {
-	server, etag, err := client.GetServer()
-	if err != nil {
-		return nil, err
-	}
 
-	// Build a function that can be used to revert the config to
-	// its original values.
-	reverter := func() error {
-		return client.UpdateServer(server.Writable(), "")
-	}
-
-	// The underlying code expects all values to be string, even if when
-	// using preseed the yaml.v2 package unmarshals them as integers.
-	for key, value := range config {
-		if number, ok := value.(int); ok {
-			value = strconv.Itoa(number)
+	// Apply network configuration
+	if config.Networks != nil && len(config.Networks) > 0 {
+		// Get the list of networks
+		networkNames, err := d.GetNetworkNames()
+		if err != nil {
+			return errors.Wrap(err, "Failed to retrieve list of networks")
 		}
-		config[key] = value
-	}
 
-	err = client.UpdateServer(api.ServerPut{Config: config}, etag)
-	if err != nil {
-		return nil, err
-	}
+		// Network creator
+		createNetwork := func(network api.NetworksPost) error {
+			// Create the network if doesn't exist
+			err := d.CreateNetwork(network)
+			if err != nil {
+				return errors.Wrapf(err, "Failed to create network '%s'", network.Name)
+			}
 
-	// Updating the server was successful, so return the reverter function
-	// in case it's needed later.
-	return reverter, nil
-}
+			// Setup reverter
+			reverts = append(reverts, func() {
+				d.DeleteNetwork(network.Name)
+			})
 
-// Turn on clustering.
-func (cmd *CmdInit) initCluster(client lxd.ContainerServer, put api.ClusterPut, password string) (reverter, error) {
-	var reverter func() error
-	var op *lxd.Operation
-	var err error
-	if put.ClusterAddress == "" {
-		op, err = client.UpdateCluster(put, "")
-		if err != nil {
-			return nil, err
+			return nil
 		}
-	} else {
-		// If a password was provided, try to make the joining node's
-		// certificate trusted by the cluster.
-		if password != "" {
-			server, _, err := client.GetServer()
+
+		// Network updater
+		updateNetwork := func(network api.NetworksPost) error {
+			// Get the current network
+			currentNetwork, etag, err := d.GetNetwork(network.Name)
 			if err != nil {
-				return nil, errors.Wrap(err, "failed to get joining node's server info")
+				return errors.Wrapf(err, "Failed to retrieve current network '%s'", network.Name)
 			}
-			err = cluster.SetupTrust(
-				server.Environment.Certificate, put.ClusterAddress, put.ClusterCertificate,
-				password)
+
+			// Setup reverter
+			reverts = append(reverts, func() {
+				d.UpdateNetwork(currentNetwork.Name, currentNetwork.Writable(), "")
+			})
+
+			// Prepare the update
+			newNetwork := api.NetworkPut{}
+			err = shared.DeepCopy(currentNetwork.Writable(), &newNetwork)
 			if err != nil {
-				return nil, errors.Wrap(err, "failed to register joining node's certificate")
+				return errors.Wrapf(err, "Failed to copy configuration of network '%s'", network.Name)
 			}
-		}
 
-		op, err = client.UpdateCluster(put, "")
-		if err != nil {
-			return nil, err
-		}
-	}
-	err = op.Wait()
-	if err != nil {
-		return nil, err
-	}
-	return reverter, nil
-}
-
-// Create or update a single pool, and return a revert function in case of success.
-func (cmd *CmdInit) initPool(client lxd.ContainerServer, pool api.StoragePoolsPost) (reverter, error) {
-	var reverter func() error
-	currentPool, _, err := client.GetStoragePool(pool.Name)
-	if err == nil {
-		reverter, err = cmd.initPoolUpdate(client, pool, currentPool.Writable())
-	} else {
-		reverter, err = cmd.initPoolCreate(client, pool)
-	}
-	if err != nil {
-		return nil, err
-	}
-	return reverter, nil
-}
+			// Description override
+			if network.Description != "" {
+				newNetwork.Description = network.Description
+			}
 
-// Create a single new pool, and return a revert function to delete it.
-func (cmd *CmdInit) initPoolCreate(client lxd.ContainerServer, pool api.StoragePoolsPost) (reverter, error) {
-	reverter := func() error {
-		return client.DeleteStoragePool(pool.Name)
-	}
-	err := client.CreateStoragePool(pool)
-	return reverter, err
-}
+			// Config overrides
+			for k, v := range network.Config {
+				newNetwork.Config[k] = fmt.Sprintf("%v", v)
+			}
 
-// Update a single pool, and return a function that can be used to
-// revert it to its original state.
-func (cmd *CmdInit) initPoolUpdate(client lxd.ContainerServer, pool api.StoragePoolsPost, currentPool api.StoragePoolPut) (reverter, error) {
-	reverter := func() error {
-		return client.UpdateStoragePool(pool.Name, currentPool, "")
-	}
-	err := client.UpdateStoragePool(pool.Name, api.StoragePoolPut{
-		Config: pool.Config,
-	}, "")
-	return reverter, err
-}
+			// Apply it
+			err = d.UpdateNetwork(currentNetwork.Name, newNetwork, etag)
+			if err != nil {
+				return errors.Wrapf(err, "Failed to update network '%s'", network.Name)
+			}
 
-// Create or update a single network, and return a revert function in case of success.
-func (cmd *CmdInit) initNetwork(client lxd.ContainerServer, network api.NetworksPost) (reverter, error) {
-	var revert func() error
-	currentNetwork, _, err := client.GetNetwork(network.Name)
-	if err == nil {
-		// Sanity check, make sure the network type being updated
-		// is still "bridge", which is the only type the existing
-		// network can have.
-		if network.Type != "" && network.Type != "bridge" {
-			return nil, fmt.Errorf("Only 'bridge' type networks are supported")
+			return nil
 		}
-		revert, err = cmd.initNetworkUpdate(client, network, currentNetwork.Writable())
-	} else {
-		revert, err = cmd.initNetworkCreate(client, network)
-	}
-	if err != nil {
-		return nil, err
-	}
-	return revert, nil
-}
-
-// Create a single new network, and return a revert function to delete it.
-func (cmd *CmdInit) initNetworkCreate(client lxd.ContainerServer, network api.NetworksPost) (reverter, error) {
-	reverter := func() error {
-		return client.DeleteNetwork(network.Name)
-	}
-	err := client.CreateNetwork(network)
-	return reverter, err
-}
-
-// Update a single network, and return a function that can be used to
-// revert it to its original state.
-func (cmd *CmdInit) initNetworkUpdate(client lxd.ContainerServer, network api.NetworksPost, currentNetwork api.NetworkPut) (reverter, error) {
-	reverter := func() error {
-		return client.UpdateNetwork(network.Name, currentNetwork, "")
-	}
-	err := client.UpdateNetwork(network.Name, api.NetworkPut{
-		Config: network.Config,
-	}, "")
-	return reverter, err
-}
-
-// Create or update a single profile, and return a revert function in case of success.
-func (cmd *CmdInit) initProfile(client lxd.ContainerServer, profile api.ProfilesPost) (reverter, error) {
-	var reverter func() error
-	currentProfile, _, err := client.GetProfile(profile.Name)
-	if err == nil {
-		reverter, err = cmd.initProfileUpdate(client, profile, currentProfile.Writable())
-	} else {
-		reverter, err = cmd.initProfileCreate(client, profile)
-	}
-	if err != nil {
-		return nil, err
-	}
-	return reverter, nil
-}
-
-// Create a single new profile, and return a revert function to delete it.
-func (cmd *CmdInit) initProfileCreate(client lxd.ContainerServer, profile api.ProfilesPost) (reverter, error) {
-	reverter := func() error {
-		return client.DeleteProfile(profile.Name)
-	}
-	err := client.CreateProfile(profile)
-	return reverter, err
-}
-
-// Update a single profile, and return a function that can be used to
-// revert it to its original state.
-func (cmd *CmdInit) initProfileUpdate(client lxd.ContainerServer, profile api.ProfilesPost, currentProfile api.ProfilePut) (reverter, error) {
-	reverter := func() error {
-		return client.UpdateProfile(profile.Name, currentProfile, "")
-	}
-	err := client.UpdateProfile(profile.Name, api.ProfilePut{
-		Config:      profile.Config,
-		Description: profile.Description,
-		Devices:     profile.Devices,
-	}, "")
-	return reverter, err
-}
 
-// Check that the arguments passed via command line are consistent,
-// and no invalid combination is provided.
-func (cmd *CmdInit) validateArgs() error {
-	if cmd.Args.Auto && cmd.Args.Preseed {
-		return fmt.Errorf("Non-interactive mode supported by only one of --auto or --preseed")
-	}
-	if !cmd.Args.Auto {
-		if cmd.Args.StorageBackend != "" || cmd.Args.StorageCreateDevice != "" || cmd.Args.StorageCreateLoop != -1 || cmd.Args.StorageDataset != "" || cmd.Args.NetworkAddress != "" || cmd.Args.NetworkPort != -1 || cmd.Args.TrustPassword != "" {
-			return fmt.Errorf("Init configuration is only valid with --auto")
-		}
-	}
-	return nil
-}
+		for _, network := range config.Networks {
+			// New network
+			if !shared.StringInSlice(network.Name, networkNames) {
+				err := createNetwork(network)
+				if err != nil {
+					return err
+				}
 
-// Check that the arguments passed along with --auto are valid and consistent.
-// and no invalid combination is provided.
-func (cmd *CmdInit) validateArgsAuto(availableStoragePoolsDrivers []string) error {
-	if !shared.StringInSlice(cmd.Args.StorageBackend, supportedStoragePoolDrivers) {
-		return fmt.Errorf("The requested backend '%s' isn't supported by lxd init.", cmd.Args.StorageBackend)
-	}
-	if !shared.StringInSlice(cmd.Args.StorageBackend, availableStoragePoolsDrivers) {
-		return fmt.Errorf("The requested backend '%s' isn't available on your system (missing tools).", cmd.Args.StorageBackend)
-	}
+				continue
+			}
 
-	if cmd.Args.StorageBackend == "dir" {
-		if cmd.Args.StorageCreateLoop != -1 || cmd.Args.StorageCreateDevice != "" || cmd.Args.StorageDataset != "" {
-			return fmt.Errorf("None of --storage-pool, --storage-create-device or --storage-create-loop may be used with the 'dir' backend.")
-		}
-	} else {
-		if cmd.Args.StorageCreateLoop != -1 && cmd.Args.StorageCreateDevice != "" {
-			return fmt.Errorf("Only one of --storage-create-device or --storage-create-loop can be specified.")
+			// Existing network
+			err := updateNetwork(network)
+			if err != nil {
+				return err
+			}
 		}
 	}
 
-	if cmd.Args.NetworkAddress == "" {
-		if cmd.Args.NetworkPort != -1 {
-			return fmt.Errorf("--network-port cannot be used without --network-address.")
-		}
-		if cmd.Args.TrustPassword != "" {
-			return fmt.Errorf("--trust-password cannot be used without --network-address.")
+	// Apply storage configuration
+	if config.StoragePools != nil && len(config.StoragePools) > 0 {
+		// Get the list of storagePools
+		storagePoolNames, err := d.GetStoragePoolNames()
+		if err != nil {
+			return errors.Wrap(err, "Failed to retrieve list of storage pools")
 		}
-	}
-
-	return nil
-}
-
-// Return the available storage pools drivers (depending on installed tools).
-func (cmd *CmdInit) availableStoragePoolsDrivers() []string {
-	drivers := []string{"dir"}
-
-	backingFs, err := util.FilesystemDetect(shared.VarPath())
-	if err != nil {
-		backingFs = "dir"
-	}
 
-	// Check available backends
-	for _, driver := range supportedStoragePoolDrivers {
-		if driver == "dir" {
-			continue
-		}
+		// StoragePool creator
+		createStoragePool := func(storagePool api.StoragePoolsPost) error {
+			// Create the storagePool if doesn't exist
+			err := d.CreateStoragePool(storagePool)
+			if err != nil {
+				return errors.Wrapf(err, "Failed to create storage pool '%s'", storagePool.Name)
+			}
 
-		// btrfs can work in user namespaces too. (If
-		// source=/some/path/on/btrfs is used.)
-		if cmd.RunningInUserns && (backingFs != "btrfs" || driver != "btrfs") {
-			continue
-		}
+			// Setup reverter
+			reverts = append(reverts, func() {
+				d.DeleteStoragePool(storagePool.Name)
+			})
 
-		// Initialize a core storage interface for the given driver.
-		_, err := storageCoreInit(driver)
-		if err != nil {
-			continue
+			return nil
 		}
 
-		drivers = append(drivers, driver)
-	}
-	return drivers
-}
-
-// Return an error if the given profile has already a device with the
-// given name.
-func (cmd *CmdInit) profileDeviceAlreadyExists(profile *api.ProfilesPost, deviceName string) error {
-	_, ok := profile.Devices[deviceName]
-	if ok {
-		return fmt.Errorf("Device already exists: %s", deviceName)
-	}
-	return nil
-}
+		// StoragePool updater
+		updateStoragePool := func(storagePool api.StoragePoolsPost) error {
+			// Get the current storagePool
+			currentStoragePool, etag, err := d.GetStoragePool(storagePool.Name)
+			if err != nil {
+				return errors.Wrapf(err, "Failed to retrieve current storage pool '%s'", storagePool.Name)
+			}
 
-// Ask if the user wants to enable clustering
-func (cmd *CmdInit) askClustering() (*cmdInitClusteringParams, error) {
-	askWants := "Would you like to use LXD clustering? (yes/no) [default=no]: "
-	if !cmd.Context.AskBool(askWants, "no") {
-		return nil, nil
-	}
+			// Sanity check
+			if currentStoragePool.Driver != storagePool.Driver {
+				return fmt.Errorf("Storage pool '%s' is of type '%s' instead of '%s'", currentStoragePool.Name, currentStoragePool.Driver, storagePool.Driver)
+			}
 
-	params := &cmdInitClusteringParams{}
+			// Setup reverter
+			reverts = append(reverts, func() {
+				d.UpdateStoragePool(currentStoragePool.Name, currentStoragePool.Writable(), "")
+			})
 
-	// Node name
-	hostname, err := os.Hostname()
-	if err != nil {
-		hostname = "lxd"
-	}
-	askName := fmt.Sprintf(
-		"What name should be used to identify this node in the cluster? [default=%s]: ",
-		hostname)
-	params.Name = cmd.Context.AskString(askName, hostname, nil)
-
-	// Network address
-	address := util.NetworkInterfaceAddress()
-	askAddress := fmt.Sprintf(
-		"What IP address or DNS name should be used to reach this node? [default=%s]: ",
-		address)
-	address = util.CanonicalNetworkAddress(cmd.Context.AskString(askAddress, address, nil))
-	host, port, err := net.SplitHostPort(address)
-	if err != nil {
-		return nil, err
-	}
-	portN, err := strconv.Atoi(port)
-	if err != nil {
-		return nil, err
-	}
-	params.Address = host
-	params.Port = int64(portN)
-
-	// Join existing cluster
-	if !cmd.Context.AskBool("Are you joining an existing cluster? (yes/no) [default=no]: ", "no") {
-		params.TrustPassword = cmd.Context.AskPassword(
-			"Trust password for new clients: ", cmd.PasswordReader)
-		return params, nil
-	}
+			// Prepare the update
+			newStoragePool := api.StoragePoolPut{}
+			err = shared.DeepCopy(currentStoragePool.Writable(), &newStoragePool)
+			if err != nil {
+				return errors.Wrapf(err, "Failed to copy configuration of storage pool '%s'", storagePool.Name)
+			}
 
-	// Target node address, password and certificate.
-join:
-	targetAddress := cmd.Context.AskString("IP address or FQDN of an existing cluster node: ", "", nil)
-	params.TargetAddress = util.CanonicalNetworkAddress(targetAddress)
+			// Description override
+			if storagePool.Description != "" {
+				newStoragePool.Description = storagePool.Description
+			}
 
-	url := fmt.Sprintf("https://%s", params.TargetAddress)
-	certificate, err := shared.GetRemoteCertificate(url)
-	if err != nil {
-		cmd.Context.Output("Error connecting to existing cluster node: %v\n", err)
-		goto join
-	}
-	digest := shared.CertFingerprint(certificate)
+			// Config overrides
+			for k, v := range storagePool.Config {
+				newStoragePool.Config[k] = fmt.Sprintf("%v", v)
+			}
 
-	params.TargetPassword = cmd.Context.AskPasswordOnce(
-		fmt.Sprintf("Trust password for node with fingerprint %s: ", digest), cmd.PasswordReader)
+			// Apply it
+			err = d.UpdateStoragePool(currentStoragePool.Name, newStoragePool, etag)
+			if err != nil {
+				return errors.Wrapf(err, "Failed to update storage pool '%s'", storagePool.Name)
+			}
 
-	params.TargetCert = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certificate.Raw})
+			return nil
+		}
 
-	// Confirm wipe this node
-	askConfirm := ("All existing data is lost when joining a cluster, " +
-		"continue? (yes/no) [default=no] ")
-	if !cmd.Context.AskBool(askConfirm, "") {
-		return nil, fmt.Errorf("User did not confirm erasing data")
-	}
+		for _, storagePool := range config.StoragePools {
+			// New storagePool
+			if !shared.StringInSlice(storagePool.Name, storagePoolNames) {
+				err := createStoragePool(storagePool)
+				if err != nil {
+					return err
+				}
 
-	return params, nil
-}
+				continue
+			}
 
-func (cmd *CmdInit) askClusteringStoragePools(targetPools []api.StoragePool) ([]api.StoragePoolsPost, error) {
-	pools := make([]api.StoragePoolsPost, 0)
-	for _, pool := range targetPools {
-		if pool.Status == "PENDING" {
-			continue // Skip pending pools
-		}
-		if pool.Driver == "ceph" {
-			continue // Skip ceph pools since they have no node-specific key
-		}
-		post := api.StoragePoolsPost{}
-		post.Name = pool.Name
-		post.Driver = pool.Driver
-		post.Config = pool.Config
-		// Only ask for the node-specific "source" key if it's defined
-		// in the target node.
-		if pool.Config["source"] != "" {
-			key := "source"
-			question := fmt.Sprintf(
-				`Enter local value for key "%s" of storage pool "%s": `, key, post.Name)
-			// Dummy validator for allowing empty strings.
-			validator := func(string) error { return nil }
-			post.Config[key] = cmd.Context.AskString(question, "", validator)
+			// Existing storagePool
+			err := updateStoragePool(storagePool)
+			if err != nil {
+				return err
+			}
 		}
-		pools = append(pools, post)
 	}
-	return pools, nil
-}
 
-func (cmd *CmdInit) askClusteringNetworks(targetNetworks []api.Network) ([]api.NetworksPost, error) {
-	networks := make([]api.NetworksPost, 0)
-	for _, network := range targetNetworks {
-		if !network.Managed || network.Status == "PENDING" {
-			continue // Skip not-managed or pending networks
-		}
-		post := api.NetworksPost{}
-		post.Name = network.Name
-		post.Config = network.Config
-		post.Type = network.Type
-		post.Managed = true
-		// Only ask for the node-specific "bridge.external_interfaces"
-		// key if it's defined in the target node.
-		if network.Config["bridge.external_interfaces"] != "" {
-			key := "bridge.external_interfaces"
-			question := fmt.Sprintf(
-				`Enter local value for key "%s" of network "%s": `, key, post.Name)
-			// Dummy validator for allowing empty strings.
-			validator := func(string) error { return nil }
-			post.Config[key] = cmd.Context.AskString(question, "", validator)
+	// Apply profile configuration
+	if config.Profiles != nil && len(config.Profiles) > 0 {
+		// Get the list of profiles
+		profileNames, err := d.GetProfileNames()
+		if err != nil {
+			return errors.Wrap(err, "Failed to retrieve list of profiles")
 		}
-		networks = append(networks, post)
-	}
-	return networks, nil
-}
 
-// Ask if the user wants to create a new storage pool, and return
-// the relevant parameters if so.
-func (cmd *CmdInit) askStorage(client lxd.ContainerServer, existingPools []string, availableBackends []string) (*cmdInitStorageParams, error) {
-	if !cmd.Context.AskBool("Do you want to configure a new storage pool (yes/no) [default=yes]? ", "yes") {
-		return nil, nil
-	}
-	storage := &cmdInitStorageParams{
-		Config: map[string]string{},
-	}
-
-	backingFs, err := util.FilesystemDetect(shared.VarPath())
-	if err != nil {
-		backingFs = "dir"
-	}
-
-	defaultStorage := "dir"
-	if backingFs == "btrfs" && shared.StringInSlice("btrfs", availableBackends) {
-		defaultStorage = "btrfs"
-	} else if shared.StringInSlice("zfs", availableBackends) {
-		defaultStorage = "zfs"
-	} else if shared.StringInSlice("btrfs", availableBackends) {
-		defaultStorage = "btrfs"
-	}
-
-	for {
-		storage.LoopSize = -1
-		storage.Pool = cmd.Context.AskString("Name of the new storage pool [default=default]: ", "default", nil)
-		if shared.StringInSlice(storage.Pool, existingPools) {
-			fmt.Printf("The requested storage pool \"%s\" already exists. Please choose another name.\n", storage.Pool)
-			// Ask the user again if hew wants to create a
-			// storage pool.
-			continue
-		}
+		// Profile creator
+		createProfile := func(profile api.ProfilesPost) error {
+			// Create the profile if doesn't exist
+			err := d.CreateProfile(profile)
+			if err != nil {
+				return errors.Wrapf(err, "Failed to create profile '%s'", profile.Name)
+			}
 
-		storage.Backend = cmd.Context.AskChoice(fmt.Sprintf("Name of the storage backend to use (%s) [default=%s]: ", strings.Join(availableBackends, ", "), defaultStorage), supportedStoragePoolDrivers, defaultStorage)
+			// Setup reverter
+			reverts = append(reverts, func() {
+				d.DeleteProfile(profile.Name)
+			})
 
-		// XXX The following to checks don't make much sense, since
-		// AskChoice will always re-ask the question if the answer
-		// is not among supportedStoragePoolDrivers. It seems legacy
-		// code that we should drop?
-		if !shared.StringInSlice(storage.Backend, supportedStoragePoolDrivers) {
-			return nil, fmt.Errorf("The requested backend '%s' isn't supported by lxd init.", storage.Backend)
+			return nil
 		}
 
-		// XXX Instead of manually checking if the provided choice is
-		// among availableBackends, we could just pass to askChoice the
-		// availableBackends list instead of supportedStoragePoolDrivers.
-		if !shared.StringInSlice(storage.Backend, availableBackends) {
-			return nil, fmt.Errorf("The requested backend '%s' isn't available on your system (missing tools).", storage.Backend)
-		}
+		// Profile updater
+		updateProfile := func(profile api.ProfilesPost) error {
+			// Get the current profile
+			currentProfile, etag, err := d.GetProfile(profile.Name)
+			if err != nil {
+				return errors.Wrapf(err, "Failed to retrieve current profile '%s'", profile.Name)
+			}
 
-		if storage.Backend == "dir" {
-			break
-		}
+			// Setup reverter
+			reverts = append(reverts, func() {
+				d.UpdateProfile(currentProfile.Name, currentProfile.Writable(), "")
+			})
 
-		// Optimization for btrfs on btrfs
-		if storage.Backend == "btrfs" && backingFs == "btrfs" {
-			if cmd.Context.AskBool(fmt.Sprintf("Would you like to create a new btrfs subvolume under %s (yes/no) [default=yes]: ", shared.VarPath("")), "yes") {
-				storage.Dataset = shared.VarPath("storage-pools", storage.Pool)
-				break
+			// Prepare the update
+			newProfile := api.ProfilePut{}
+			err = shared.DeepCopy(currentProfile.Writable(), &newProfile)
+			if err != nil {
+				return errors.Wrapf(err, "Failed to copy configuration of profile '%s'", profile.Name)
 			}
-		}
-
-		question := fmt.Sprintf("Create a new %s pool (yes/no) [default=yes]? ", strings.ToUpper(storage.Backend))
-		if cmd.Context.AskBool(question, "yes") {
-			if storage.Backend == "ceph" {
-				// Pool configuration
-				if storage.Config != nil {
-					storage.Config = map[string]string{}
-				}
 
-				// ask for the name of the cluster
-				storage.Config["ceph.cluster_name"] = cmd.Context.AskString("Name of the existing CEPH cluster [default=ceph]: ", "ceph", nil)
+			// Description override
+			if profile.Description != "" {
+				newProfile.Description = profile.Description
+			}
 
-				// ask for the name of the osd pool
-				storage.Config["ceph.osd.pool_name"] = cmd.Context.AskString("Name of the OSD storage pool [default=lxd]: ", "lxd", nil)
+			// Config overrides
+			for k, v := range profile.Config {
+				newProfile.Config[k] = fmt.Sprintf("%v", v)
+			}
 
-				// ask for the number of placement groups
-				storage.Config["ceph.osd.pg_num"] = cmd.Context.AskString("Number of placement groups [default=32]: ", "32", nil)
-			} else if cmd.Context.AskBool("Would you like to use an existing block device (yes/no) [default=no]? ", "no") {
-				deviceExists := func(path string) error {
-					if !shared.IsBlockdevPath(path) {
-						return fmt.Errorf("'%s' is not a block device", path)
-					}
-					return nil
-				}
-				storage.Device = cmd.Context.AskString("Path to the existing block device: ", "", deviceExists)
-			} else {
-				st := syscall.Statfs_t{}
-				err := syscall.Statfs(shared.VarPath(), &st)
-				if err != nil {
-					return nil, fmt.Errorf("couldn't statfs %s: %s", shared.VarPath(), err)
+			// Device overrides
+			for k, v := range profile.Devices {
+				// New device
+				_, ok := newProfile.Devices[k]
+				if !ok {
+					newProfile.Devices[k] = v
+					continue
 				}
 
-				/* choose 15 GB < x < 100GB, where x is 20% of the disk size */
-				defaultSize := uint64(st.Frsize) * st.Blocks / (1024 * 1024 * 1024) / 5
-				if defaultSize > 100 {
-					defaultSize = 100
-				}
-				if defaultSize < 15 {
-					defaultSize = 15
+				// Existing device
+				for configKey, configValue := range v {
+					newProfile.Devices[k][configKey] = fmt.Sprintf("%v", configValue)
 				}
-
-				question := fmt.Sprintf("Size in GB of the new loop device (1GB minimum) [default=%dGB]: ", defaultSize)
-				storage.LoopSize = cmd.Context.AskInt(question, 1, -1, fmt.Sprintf("%d", defaultSize))
 			}
-		} else {
-			if storage.Backend == "ceph" {
-				// Pool configuration
-				if storage.Config != nil {
-					storage.Config = map[string]string{}
-				}
-
-				// ask for the name of the cluster
-				storage.Config["ceph.cluster_name"] = cmd.Context.AskString("Name of the existing CEPH cluster [default=ceph]: ", "ceph", nil)
 
-				// ask for the name of the existing pool
-				storage.Config["source"] = cmd.Context.AskString("Name of the existing OSD storage pool [default=lxd]: ", "lxd", nil)
-				storage.Config["ceph.osd.pool_name"] = storage.Config["source"]
-			} else {
-				question := fmt.Sprintf("Name of the existing %s pool or dataset: ", strings.ToUpper(storage.Backend))
-				storage.Dataset = cmd.Context.AskString(question, "", nil)
+			// Apply it
+			err = d.UpdateProfile(currentProfile.Name, newProfile, etag)
+			if err != nil {
+				return errors.Wrapf(err, "Failed to update profile '%s'", profile.Name)
 			}
+
+			return nil
 		}
 
-		if storage.Backend == "lvm" {
-			_, err := exec.LookPath("thin_check")
-			if err != nil {
-				fmt.Printf(`
-The LVM thin provisioning tools couldn't be found. LVM can still be used
-without thin provisioning but this will disable over-provisioning,
-increase the space requirements and creation time of images, containers
-and snapshots.
-
-If you wish to use thin provisioning, abort now, install the tools from
-your Linux distribution and run "lxd init" again afterwards.
-
-`)
-				if !cmd.Context.AskBool("Do you want to continue without thin provisioning? (yes/no) [default=yes]: ", "yes") {
-					return nil, fmt.Errorf("The LVM thin provisioning tools couldn't be found on the system.")
+		for _, profile := range config.Profiles {
+			// New profile
+			if !shared.StringInSlice(profile.Name, profileNames) {
+				err := createProfile(profile)
+				if err != nil {
+					return err
 				}
 
-				storage.Config["lvm.use_thinpool"] = "false"
+				continue
 			}
-		}
-
-		break
-	}
-	return storage, nil
-}
-
-// If we detect that we are running inside an unprivileged container,
-// ask if the user wants to the default profile to be a privileged
-// one.
-func (cmd *CmdInit) askDefaultPrivileged() int {
-	// Detect lack of uid/gid
-	defaultPrivileged := -1
-	needPrivileged := false
-	idmapset, err := idmap.DefaultIdmapSet("")
-	if err != nil || len(idmapset.Idmap) == 0 || idmapset.Usable() != nil {
-		needPrivileged = true
-	}
-
-	if cmd.RunningInUserns && needPrivileged {
-		fmt.Printf(`
-We detected that you are running inside an unprivileged container.
-This means that unless you manually configured your host otherwise,
-you will not have enough uid and gid to allocate to your containers.
-
-LXD can re-use your container's own allocation to avoid the problem.
-Doing so makes your nested containers slightly less safe as they could
-in theory attack their parent container and gain more privileges than
-they otherwise would.
-
-`)
 
-		if cmd.Context.AskBool("Would you like to have your containers share their parent's allocation (yes/no) [default=yes]? ", "yes") {
-			defaultPrivileged = 1
-		} else {
-			defaultPrivileged = 0
+			// Existing profile
+			err := updateProfile(profile)
+			if err != nil {
+				return err
+			}
 		}
 	}
-	return defaultPrivileged
-}
-
-// Ask if the user wants to expose LXD over the network, and collect
-// the relevant parameters if so.
-func (cmd *CmdInit) askNetworking() *cmdInitNetworkingParams {
-	if !cmd.Context.AskBool("Would you like LXD to be available over the network (yes/no) [default=no]? ", "no") {
-		return nil
-	}
-	networking := &cmdInitNetworkingParams{}
 
-	isIPAddress := func(s string) error {
-		if s != "all" && net.ParseIP(s) == nil {
-			return fmt.Errorf("'%s' is not an IP address", s)
+	// Apply clustering configuration
+	if config.Cluster != nil && config.Cluster.Enabled {
+		// Get the current cluster configuration
+		currentCluster, etag, err := d.GetCluster()
+		if err != nil {
+			return errors.Wrap(err, "Failed to retrieve current cluster config")
 		}
-		return nil
-	}
-
-	networking.Address = cmd.Context.AskString("Address to bind LXD to (not including port) [default=all]: ", "all", isIPAddress)
-	if networking.Address == "all" {
-		networking.Address = "::"
-	}
-
-	if net.ParseIP(networking.Address).To4() == nil {
-		networking.Address = fmt.Sprintf("[%s]", networking.Address)
-	}
-	networking.Port = cmd.Context.AskInt("Port to bind LXD to [default=8443]: ", 1, 65535, "8443")
-	networking.TrustPassword = cmd.Context.AskPassword("Trust password for new clients: ", cmd.PasswordReader)
 
-	return networking
-}
-
-// Ask if the user wants images to be automatically refreshed.
-func (cmd *CmdInit) askImages() bool {
-	return cmd.Context.AskBool("Would you like stale cached images to be updated automatically (yes/no) [default=yes]? ", "yes")
-}
-
-// Ask if the user wants to create a new network bridge, and return
-// the relevant parameters if so.
-func (cmd *CmdInit) askBridge(client lxd.ContainerServer) *cmdInitBridgeParams {
-	if !cmd.Context.AskBool("Would you like to create a new network bridge (yes/no) [default=yes]? ", "yes") {
-		return nil
-	}
+		// Check if already enabled
+		if !currentCluster.Enabled {
+			// Setup trust relationship
+			if config.Cluster.ClusterPassword != "" {
+				// Get our certificate
+				serverConfig, _, err := d.GetServer()
+				if err != nil {
+					return errors.Wrap(err, "Failed to retrieve server configuration")
+				}
 
-	bridge := &cmdInitBridgeParams{}
-	for {
-		bridge.Name = cmd.Context.AskString("What should the new bridge be called [default=lxdbr0]? ", "lxdbr0", networkValidName)
-		_, _, err := client.GetNetwork(bridge.Name)
-		if err == nil {
-			fmt.Printf("The requested network bridge \"%s\" already exists. Please choose another name.\n", bridge.Name)
-			// Ask the user again if hew wants to create a
-			// storage pool.
-			continue
-		}
-		bridge.IPv4 = cmd.Context.AskString("What IPv4 address should be used (CIDR subnet notation, “auto” or “none”) [default=auto]? ", "auto", func(value string) error {
-			if shared.StringInSlice(value, []string{"auto", "none"}) {
-				return nil
+				// Try to setup trust
+				err = cluster.SetupTrust(serverConfig.Environment.Certificate, config.Cluster.ClusterAddress,
+					config.Cluster.ClusterCertificate, config.Cluster.ClusterPassword)
+				if err != nil {
+					return errors.Wrap(err, "Failed to setup cluster trust")
+				}
 			}
-			return networkValidAddressCIDRV4(value)
-		})
 
-		if !shared.StringInSlice(bridge.IPv4, []string{"auto", "none"}) {
-			bridge.IPv4Nat = cmd.Context.AskBool("Would you like LXD to NAT IPv4 traffic on your bridge? [default=yes]? ", "yes")
-		}
-
-		bridge.IPv6 = cmd.Context.AskString("What IPv6 address should be used (CIDR subnet notation, “auto” or “none”) [default=auto]? ", "auto", func(value string) error {
-			if shared.StringInSlice(value, []string{"auto", "none"}) {
-				return nil
+			// Configure the cluster
+			op, err := d.UpdateCluster(config.Cluster.ClusterPut, etag)
+			if err != nil {
+				return errors.Wrap(err, "Failed to configure cluster")
 			}
-			return networkValidAddressCIDRV6(value)
-		})
 
-		if !shared.StringInSlice(bridge.IPv6, []string{"auto", "none"}) {
-			bridge.IPv6Nat = cmd.Context.AskBool("Would you like LXD to NAT IPv6 traffic on your bridge? [default=yes]? ", "yes")
+			err = op.Wait()
+			if err != nil {
+				return errors.Wrap(err, "Failed to configure cluster")
+			}
 		}
-		break
 	}
-	return bridge
-}
 
-// Defines the schema for all possible configuration knobs supported by the
-// lxd init command, either directly fed via --preseed or populated by
-// the auto/interactive modes.
-type cmdInitData struct {
-	api.ServerPut   `yaml:",inline"`
-	Pools           []api.StoragePoolsPost `yaml:"storage_pools"`
-	Networks        []api.NetworksPost
-	Profiles        []api.ProfilesPost
-	Cluster         api.ClusterPut
-	ClusterPassword string `yaml:"cluster_password"`
-}
-
-// Parameters needed when enbling clustering in interactive mode.
-type cmdInitClusteringParams struct {
-	Name           string // Name of the new node
-	Address        string // Network address of the new node
-	Port           int64  // Network port of the new node
-	TrustPassword  string // Trust password
-	TargetAddress  string // Network address of cluster node to join.
-	TargetCert     []byte // Public key of the cluster to join.
-	TargetPassword string // Trust password of the cluster to join.
-}
-
-// Parameters needed when creating a storage pool in interactive or auto
-// mode.
-type cmdInitStorageParams struct {
-	Backend  string            // == supportedStoragePoolDrivers
-	LoopSize int64             // Size in GB
-	Device   string            // Path
-	Pool     string            // pool name
-	Dataset  string            // existing ZFS pool name
-	Config   map[string]string // Additional pool configuration
-}
-
-// Parameters needed when configuring the LXD server networking options in interactive
-// mode or auto mode.
-type cmdInitNetworkingParams struct {
-	Address       string // Address
-	Port          int64  // Port
-	TrustPassword string // Trust password
-}
-
-// Parameters needed when creating a bridge network device in interactive
-// mode.
-type cmdInitBridgeParams struct {
-	Name    string // Bridge name
-	IPv4    string // IPv4 address
-	IPv4Nat bool   // IPv4 address
-	IPv6    string // IPv6 address
-	IPv6Nat bool   // IPv6 address
-}
-
-// Shortcut for closure/anonymous functions that are meant to revert
-// some change, and that are passed around as parameters.
-type reverter func() error
-
-func cmdInit(args *Args) error {
-	command := &CmdInit{
-		Context:         cmd.DefaultContext(),
-		Args:            args,
-		RunningInUserns: shared.RunningInUserNS(),
-		VarDir:          "",
-		PasswordReader:  terminal.ReadPassword,
-	}
-	return command.Run()
+	revert = false
+	return nil
 }
diff --git a/lxd/main_init_auto.go b/lxd/main_init_auto.go
new file mode 100644
index 000000000..c7dbe95ab
--- /dev/null
+++ b/lxd/main_init_auto.go
@@ -0,0 +1,114 @@
+package main
+
+import (
+	"fmt"
+
+	"github.com/pkg/errors"
+	"github.com/spf13/cobra"
+
+	"github.com/lxc/lxd/client"
+	"github.com/lxc/lxd/shared"
+	"github.com/lxc/lxd/shared/api"
+)
+
+func (c *cmdInit) RunAuto(cmd *cobra.Command, args []string, d lxd.ContainerServer) (*initData, error) {
+	// Sanity checks
+	if !shared.StringInSlice(c.flagStorageBackend, supportedStoragePoolDrivers) {
+		return nil, fmt.Errorf("The requested backend '%s' isn't supported by lxd init", c.flagStorageBackend)
+	}
+
+	if !shared.StringInSlice(c.flagStorageBackend, c.availableStorageDrivers()) {
+		return nil, fmt.Errorf("The requested backend '%s' isn't available on your system (missing tools)", c.flagStorageBackend)
+	}
+
+	if c.flagStorageBackend == "dir" || c.flagStorageBackend == "" {
+		if c.flagStorageLoopSize != -1 || c.flagStorageDevice != "" || c.flagStoragePool != "" {
+			return nil, fmt.Errorf("None of --storage-pool, --storage-create-device or --storage-create-loop may be used with the 'dir' backend")
+		}
+	} else {
+		if c.flagStorageLoopSize != -1 && c.flagStorageDevice != "" {
+			return nil, fmt.Errorf("Only one of --storage-create-device or --storage-create-loop can be specified")
+		}
+	}
+
+	if c.flagNetworkAddress == "" {
+		if c.flagNetworkPort != -1 {
+			return nil, fmt.Errorf("--network-port can't be used without --network-address")
+		}
+
+		if c.flagTrustPassword != "" {
+			return nil, fmt.Errorf("--trust-password can't be used without --network-address")
+		}
+	}
+
+	storagePools, err := d.GetStoragePoolNames()
+	if err != nil {
+		return nil, errors.Wrap(err, "Failed to retrieve list of storage pools")
+	}
+
+	if len(storagePools) > 0 && (c.flagStorageBackend != "" || c.flagStorageDevice != "" || c.flagStorageLoopSize != -1 || c.flagStoragePool != "") {
+		return nil, fmt.Errorf("Storage has already been configured")
+	}
+
+	// Defaults
+	if c.flagStorageBackend == "" {
+		c.flagStorageBackend = "dir"
+	}
+
+	if c.flagNetworkPort == -1 {
+		c.flagNetworkPort = 8443
+	}
+
+	// Fill in the configuration
+	config := initData{}
+
+	// Network listening
+	if c.flagNetworkAddress != "" {
+		config.Config["core.https_address"] = fmt.Sprintf("%s:%d", c.flagNetworkAddress, c.flagNetworkPort)
+
+		if c.flagTrustPassword != "" {
+			config.Config["core.trust_password"] = c.flagTrustPassword
+		}
+	}
+
+	// Storage configuration
+	if len(storagePools) == 0 {
+		// Storage pool
+		pool := api.StoragePoolsPost{
+			Name:   "default",
+			Driver: c.flagStorageBackend,
+		}
+		pool.Config = map[string]string{}
+
+		if c.flagStorageDevice != "" {
+			pool.Config["source"] = c.flagStorageDevice
+		} else if c.flagStorageLoopSize > 0 {
+			pool.Config["size"] = fmt.Sprintf("%dGB", c.flagStorageLoopSize)
+		} else {
+			pool.Config["source"] = c.flagStoragePool
+		}
+
+		// If using a device or loop, --storage-pool refers to the name of the new pool
+		if c.flagStoragePool != "" && (c.flagStorageDevice != "" || c.flagStorageLoopSize != -1) {
+			pool.Name = c.flagStoragePool
+		}
+
+		config.StoragePools = []api.StoragePoolsPost{pool}
+
+		// Profile entry
+		config.Profiles = []api.ProfilesPost{{
+			Name: "default",
+			ProfilePut: api.ProfilePut{
+				Devices: map[string]map[string]string{
+					"root": {
+						"type": "disk",
+						"path": "/",
+						"pool": pool.Name,
+					},
+				},
+			},
+		}}
+	}
+
+	return &config, nil
+}
diff --git a/lxd/main_init_interactive.go b/lxd/main_init_interactive.go
new file mode 100644
index 000000000..d6f5fbc3f
--- /dev/null
+++ b/lxd/main_init_interactive.go
@@ -0,0 +1,490 @@
+package main
+
+import (
+	"encoding/pem"
+	"fmt"
+	"net"
+	"os"
+	"os/exec"
+	"strings"
+	"syscall"
+
+	"github.com/pkg/errors"
+	"github.com/spf13/cobra"
+	"gopkg.in/yaml.v2"
+
+	"github.com/lxc/lxd/client"
+	"github.com/lxc/lxd/lxd/cluster"
+	"github.com/lxc/lxd/lxd/util"
+	"github.com/lxc/lxd/shared"
+	"github.com/lxc/lxd/shared/api"
+	cli "github.com/lxc/lxd/shared/cmd"
+	"github.com/lxc/lxd/shared/idmap"
+)
+
+func (c *cmdInit) RunInteractive(cmd *cobra.Command, args []string, d lxd.ContainerServer) (*initData, error) {
+	// Initialize config
+	config := initData{}
+	config.Config = map[string]interface{}{}
+	config.Networks = []api.NetworksPost{}
+	config.StoragePools = []api.StoragePoolsPost{}
+	config.Profiles = []api.ProfilesPost{
+		{
+			Name: "default",
+			ProfilePut: api.ProfilePut{
+				Config:  map[string]string{},
+				Devices: map[string]map[string]string{},
+			},
+		},
+	}
+
+	// Clustering
+	err := c.askClustering(&config, d)
+	if err != nil {
+		return nil, err
+	}
+
+	// Ask all the other questions
+	if config.Cluster == nil || config.Cluster.ClusterAddress == "" {
+		// Storage
+		err = c.askStorage(&config, d)
+		if err != nil {
+			return nil, err
+		}
+
+		// Networking
+		err = c.askNetworking(&config, d)
+		if err != nil {
+			return nil, err
+		}
+
+		// Daemon config
+		err = c.askDaemon(&config, d)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// Print the YAML
+	if cli.AskBool("Would you like a YAML \"lxd init\" preseed to be printed [default=no]? ", "no") {
+		out, err := yaml.Marshal(config)
+		if err != nil {
+			return nil, errors.Wrap(err, "Failed to render the config")
+		}
+
+		fmt.Printf("%s\n", out)
+	}
+
+	return &config, nil
+}
+
+func (c *cmdInit) askClustering(config *initData, d lxd.ContainerServer) error {
+	if cli.AskBool("Would you like to use LXD clustering? (yes/no) [default=no]: ", "no") {
+		config.Cluster = &initDataCluster{}
+		config.Cluster.Enabled = true
+
+		// Cluster server name
+		serverName, err := os.Hostname()
+		if err != nil {
+			serverName = "lxd"
+		}
+
+		config.Cluster.ServerName = cli.AskString(
+			fmt.Sprintf("What name should be used to identify this node in the cluster? [default=%s]: ", serverName), serverName, nil)
+
+		// Cluster server address
+		address := util.NetworkInterfaceAddress()
+		serverAddress := util.CanonicalNetworkAddress(cli.AskString(
+			fmt.Sprintf("What IP address or DNS name should be used to reach this node? [default=%s]: ", address), address, nil))
+		config.Config["core.https_address"] = serverAddress
+
+		if cli.AskBool("Are you joining an existing cluster? (yes/no) [default=no]: ", "no") {
+			// Existing cluster
+			for {
+				// Cluster URL
+				clusterAddress := cli.AskString("IP address or FQDN of an existing cluster node: ", "", nil)
+				_, _, err := net.SplitHostPort(clusterAddress)
+				if err != nil {
+					clusterAddress = fmt.Sprintf("%s:8443", clusterAddress)
+				}
+				config.Cluster.ClusterAddress = clusterAddress
+
+				// Cluster certificate
+				cert, err := shared.GetRemoteCertificate(fmt.Sprintf("https://%s", config.Cluster.ClusterAddress))
+				if err != nil {
+					fmt.Printf("Error connecting to existing cluster node: %v\n", err)
+					continue
+				}
+
+				certDigest := shared.CertFingerprint(cert)
+				fmt.Printf("Cluster certificate fingerprint: %s\n", certDigest)
+				if !cli.AskBool("ok? (yes/no) [default=no]: ", "no") {
+					return fmt.Errorf("User aborted configuration")
+				}
+
+				config.Cluster.ClusterCertificate = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}))
+				break
+			}
+
+			// Cluster password
+			config.Cluster.ClusterPassword = cli.AskPasswordOnce("Cluster trust password: ")
+			if !cli.AskBool("All existing data is lost when joining a cluster, continue? (yes/no) [default=no] ", "no") {
+				return fmt.Errorf("User aborted configuration")
+			}
+
+			// Connect to existing cluster
+			cert, err := util.LoadCert(shared.VarPath(""))
+			if err != nil {
+				return err
+			}
+
+			err = cluster.SetupTrust(string(cert.PublicKey()),
+				config.Cluster.ClusterAddress,
+				string(config.Cluster.ClusterCertificate), config.Cluster.ClusterPassword)
+			if err != nil {
+				return errors.Wrap(err, "Failed to setup trust relationship with cluster")
+			}
+
+			// Client parameters to connect to the target cluster node.
+			args := &lxd.ConnectionArgs{
+				TLSClientCert: string(cert.PublicKey()),
+				TLSClientKey:  string(cert.PrivateKey()),
+				TLSServerCert: string(config.Cluster.ClusterCertificate),
+			}
+
+			client, err := lxd.ConnectLXD(fmt.Sprintf("https://%s", config.Cluster.ClusterAddress), args)
+			if err != nil {
+				return err
+			}
+
+			// Prompt for storage config
+			targetPools, err := client.GetStoragePools()
+			if err != nil {
+				return errors.Wrap(err, "Failed to retrieve storage pools from the cluster")
+			}
+
+			config.StoragePools = []api.StoragePoolsPost{}
+			for _, pool := range targetPools {
+				// Skip pending pools
+				if pool.Status == "PENDING" {
+					continue
+				}
+
+				// Skip ceph pools since they have no node-specific key
+				if pool.Driver == "ceph" {
+					continue
+				}
+
+				// Setup the new local pool
+				newPool := api.StoragePoolsPost{
+					StoragePoolPut: pool.StoragePoolPut,
+					Driver:         pool.Driver,
+					Name:           pool.Name,
+				}
+
+				// Only ask for the node-specific "source" key if it's defined in the target node
+				if pool.Config["source"] != "" {
+					// Dummy validator for allowing empty strings
+					validator := func(string) error { return nil }
+					newPool.Config["source"] = cli.AskString(
+						fmt.Sprintf(`Choose the local disk or dataset for storage pool "%s" (empty for loop disk): `, pool.Name), "", validator)
+				}
+
+				config.StoragePools = append(config.StoragePools, newPool)
+			}
+
+			// Prompt for network config
+			targetNetworks, err := client.GetNetworks()
+			if err != nil {
+				return errors.Wrap(err, "failed to retrieve networks from the cluster")
+			}
+
+			config.Networks = []api.NetworksPost{}
+			for _, network := range targetNetworks {
+				// Skip not-managed or pending networks
+				if !network.Managed || network.Status == "PENDING" {
+					continue
+				}
+
+				// Setup the new local network
+				newNetwork := api.NetworksPost{
+					NetworkPut: network.NetworkPut,
+					Managed:    true,
+					Name:       network.Name,
+					Type:       network.Type,
+				}
+
+				// Only ask for the node-specific "bridge.external_interfaces" key if it's defined in the target node
+				if network.Config["bridge.external_interfaces"] != "" {
+					// Dummy validator for allowing empty strings
+					validator := func(string) error { return nil }
+					newNetwork.Config["bridge.external_interfaces"] = cli.AskString(
+						fmt.Sprintf(`Choose the local network interface to connect to network "%s" (empty for none): `, network.Name), "", validator)
+				}
+
+				config.Networks = append(config.Networks, newNetwork)
+			}
+		} else {
+			// New cluster
+			config.Cluster.ClusterPassword = cli.AskPassword("Trust password for new clients: ")
+		}
+	}
+
+	return nil
+}
+
+func (c *cmdInit) askNetworking(config *initData, d lxd.ContainerServer) error {
+	if !cli.AskBool("Would you like to create a new network bridge (yes/no) [default=yes]? ", "yes") {
+		return nil
+	}
+
+	for {
+		// Define the network
+		network := api.NetworksPost{}
+		network.Config = map[string]string{}
+
+		// Network name
+		network.Name = cli.AskString("What should the new bridge be called [default=lxdbr0]? ", "lxdbr0", networkValidName)
+		_, _, err := d.GetNetwork(network.Name)
+		if err == nil {
+			fmt.Printf("The requested network bridge \"%s\" already exists. Please choose another name.\n", network.Name)
+			continue
+		}
+
+		// Add to the default profile
+		config.Profiles[0].Devices["eth0"] = map[string]string{
+			"type":    "nic",
+			"nictype": "bridged",
+			"name":    "eth0",
+			"parent":  network.Name,
+		}
+
+		// IPv4
+		network.Config["ipv4.address"] = cli.AskString("What IPv4 address should be used (CIDR subnet notation, “auto” or “none”) [default=auto]? ", "auto", func(value string) error {
+			if shared.StringInSlice(value, []string{"auto", "none"}) {
+				return nil
+			}
+
+			return networkValidAddressCIDRV4(value)
+		})
+
+		if !shared.StringInSlice(network.Config["ipv4.address"], []string{"auto", "none"}) {
+			network.Config["ipv4.nat"] = fmt.Sprintf("%v",
+				cli.AskBool("Would you like LXD to NAT IPv4 traffic on your bridge? [default=yes]? ", "yes"))
+		}
+
+		// IPv6
+		network.Config["ipv6.address"] = cli.AskString("What IPv6 address should be used (CIDR subnet notation, “auto” or “none”) [default=auto]? ", "auto", func(value string) error {
+			if shared.StringInSlice(value, []string{"auto", "none"}) {
+				return nil
+			}
+
+			return networkValidAddressCIDRV6(value)
+		})
+
+		if !shared.StringInSlice(network.Config["ipv6.address"], []string{"auto", "none"}) {
+			network.Config["ipv6.nat"] = fmt.Sprintf("%v",
+				cli.AskBool("Would you like LXD to NAT IPv6 traffic on your bridge? [default=yes]? ", "yes"))
+		}
+
+		// Add the new network
+		config.Networks = append(config.Networks, network)
+		break
+	}
+
+	return nil
+}
+
+func (c *cmdInit) askStorage(config *initData, d lxd.ContainerServer) error {
+	if !cli.AskBool("Do you want to configure a new storage pool (yes/no) [default=yes]? ", "yes") {
+		return nil
+	}
+
+	// Figure out the preffered storage driver
+	availableBackends := c.availableStorageDrivers()
+
+	backingFs, err := util.FilesystemDetect(shared.VarPath())
+	if err != nil {
+		backingFs = "dir"
+	}
+
+	defaultStorage := "dir"
+	if backingFs == "btrfs" && shared.StringInSlice("btrfs", availableBackends) {
+		defaultStorage = "btrfs"
+	} else if shared.StringInSlice("zfs", availableBackends) {
+		defaultStorage = "zfs"
+	} else if shared.StringInSlice("btrfs", availableBackends) {
+		defaultStorage = "btrfs"
+	}
+
+	for {
+		// Define the pool
+		pool := api.StoragePoolsPost{}
+		pool.Config = map[string]string{}
+
+		pool.Name = cli.AskString("Name of the new storage pool [default=default]: ", "default", nil)
+		_, _, err := d.GetStoragePool(pool.Name)
+		if err == nil {
+			fmt.Printf("The requested storage pool \"%s\" already exists. Please choose another name.\n", pool.Name)
+			continue
+		}
+
+		// Add to the default profile
+		config.Profiles[0].Devices["root"] = map[string]string{
+			"type": "disk",
+			"path": "/",
+			"pool": pool.Name,
+		}
+
+		// Storage backend
+		pool.Driver = cli.AskChoice(
+			fmt.Sprintf("Name of the storage backend to use (%s) [default=%s]: ", strings.Join(availableBackends, ", "), defaultStorage), availableBackends, defaultStorage)
+
+		// Optimization for dir
+		if pool.Driver == "dir" {
+			config.StoragePools = append(config.StoragePools, pool)
+			break
+		}
+
+		// Optimization for btrfs on btrfs
+		if pool.Driver == "btrfs" && backingFs == "btrfs" {
+			if cli.AskBool(fmt.Sprintf("Would you like to create a new btrfs subvolume under %s (yes/no) [default=yes]: ", shared.VarPath("")), "yes") {
+				pool.Config["source"] = shared.VarPath("storage-pools", pool.Name)
+				config.StoragePools = append(config.StoragePools, pool)
+				break
+			}
+		}
+
+		if cli.AskBool(fmt.Sprintf("Create a new %s pool (yes/no) [default=yes]? ", strings.ToUpper(pool.Driver)), "yes") {
+			if pool.Driver == "ceph" {
+				// Ask for the name of the cluster
+				pool.Config["ceph.cluster_name"] = cli.AskString("Name of the existing CEPH cluster [default=ceph]: ", "ceph", nil)
+
+				// Ask for the name of the osd pool
+				pool.Config["ceph.osd.pool_name"] = cli.AskString("Name of the OSD storage pool [default=lxd]: ", "lxd", nil)
+
+				// Ask for the number of placement groups
+				pool.Config["ceph.osd.pg_num"] = cli.AskString("Number of placement groups [default=32]: ", "32", nil)
+			} else if cli.AskBool("Would you like to use an existing block device (yes/no) [default=no]? ", "no") {
+				deviceExists := func(path string) error {
+					if !shared.IsBlockdevPath(path) {
+						return fmt.Errorf("'%s' is not a block device", path)
+					}
+
+					return nil
+				}
+
+				pool.Config["source"] = cli.AskString("Path to the existing block device: ", "", deviceExists)
+			} else {
+				st := syscall.Statfs_t{}
+				err := syscall.Statfs(shared.VarPath(), &st)
+				if err != nil {
+					return errors.Wrapf(err, "Couldn't statfs %s", shared.VarPath())
+				}
+
+				/* choose 15 GB < x < 100GB, where x is 20% of the disk size */
+				defaultSize := uint64(st.Frsize) * st.Blocks / (1024 * 1024 * 1024) / 5
+				if defaultSize > 100 {
+					defaultSize = 100
+				}
+				if defaultSize < 15 {
+					defaultSize = 15
+				}
+
+				pool.Config["size"] = fmt.Sprintf("%dGB", cli.AskInt(
+					fmt.Sprintf("Size in GB of the new loop device (1GB minimum) [default=%dGB]: ", defaultSize), 1, -1, fmt.Sprintf("%d", defaultSize)))
+			}
+		} else {
+			if pool.Driver == "ceph" {
+				// ask for the name of the cluster
+				pool.Config["ceph.cluster_name"] = cli.AskString("Name of the existing CEPH cluster [default=ceph]: ", "ceph", nil)
+
+				// ask for the name of the existing pool
+				pool.Config["source"] = cli.AskString("Name of the existing OSD storage pool [default=lxd]: ", "lxd", nil)
+				pool.Config["ceph.osd.pool_name"] = pool.Config["source"]
+			} else {
+				question := fmt.Sprintf("Name of the existing %s pool or dataset: ", strings.ToUpper(pool.Driver))
+				pool.Config["source"] = cli.AskString(question, "", nil)
+			}
+		}
+
+		if pool.Driver == "lvm" {
+			_, err := exec.LookPath("thin_check")
+			if err != nil {
+				fmt.Printf(`
+The LVM thin provisioning tools couldn't be found. LVM can still be used
+without thin provisioning but this will disable over-provisioning,
+increase the space requirements and creation time of images, containers
+and snapshots.
+
+If you wish to use thin provisioning, abort now, install the tools from
+your Linux distribution and run "lxd init" again afterwards.
+
+`)
+				if !cli.AskBool("Do you want to continue without thin provisioning? (yes/no) [default=yes]: ", "yes") {
+					return fmt.Errorf("The LVM thin provisioning tools couldn't be found on the system")
+				}
+
+				pool.Config["lvm.use_thinpool"] = "false"
+			}
+		}
+
+		config.StoragePools = append(config.StoragePools, pool)
+		break
+	}
+
+	return nil
+}
+
+func (c *cmdInit) askDaemon(config *initData, d lxd.ContainerServer) error {
+	// Detect lack of uid/gid
+	idmapset, err := idmap.DefaultIdmapSet("")
+	if (err != nil || len(idmapset.Idmap) == 0 || idmapset.Usable() != nil) && shared.RunningInUserNS() {
+		fmt.Printf(`
+We detected that you are running inside an unprivileged container.
+This means that unless you manually configured your host otherwise,
+you will not have enough uid and gid to allocate to your containers.
+
+LXD can re-use your container's own allocation to avoid the problem.
+Doing so makes your nested containers slightly less safe as they could
+in theory attack their parent container and gain more privileges than
+they otherwise would.
+
+`)
+
+		if cli.AskBool("Would you like to have your containers share their parent's allocation (yes/no) [default=yes]? ", "yes") {
+			config.Profiles[0].Config["security.privileged"] = "true"
+		}
+	}
+
+	// Network listener
+	if config.Cluster == nil && cli.AskBool("Would you like LXD to be available over the network (yes/no) [default=no]? ", "no") {
+		isIPAddress := func(s string) error {
+			if s != "all" && net.ParseIP(s) == nil {
+				return fmt.Errorf("'%s' is not an IP address", s)
+			}
+
+			return nil
+		}
+
+		netAddr := cli.AskString("Address to bind LXD to (not including port) [default=all]: ", "all", isIPAddress)
+		if netAddr == "all" {
+			netAddr = "::"
+		}
+
+		if net.ParseIP(netAddr).To4() == nil {
+			netAddr = fmt.Sprintf("[%s]", netAddr)
+		}
+
+		netPort := cli.AskInt("Port to bind LXD to [default=8443]: ", 1, 65535, "8443")
+		config.Config["core.https_address"] = fmt.Sprintf("%s:%d", netAddr, netPort)
+		config.Config["core.trust_password"] = cli.AskPassword("Trust password for new clients: ")
+	}
+
+	// Ask if the user wants images to be automatically refreshed.
+	if !cli.AskBool("Would you like stale cached images to be updated automatically (yes/no) [default=yes]? ", "yes") {
+		config.Config["images.auto_update_interval"] = "0"
+	}
+
+	return nil
+}
diff --git a/lxd/main_init_preseed.go b/lxd/main_init_preseed.go
new file mode 100644
index 000000000..e21cc2034
--- /dev/null
+++ b/lxd/main_init_preseed.go
@@ -0,0 +1,29 @@
+package main
+
+import (
+	"io/ioutil"
+	"os"
+
+	"github.com/pkg/errors"
+	"github.com/spf13/cobra"
+	"gopkg.in/yaml.v2"
+
+	"github.com/lxc/lxd/client"
+)
+
+func (c *cmdInit) RunPreseed(cmd *cobra.Command, args []string, d lxd.ContainerServer) (*initData, error) {
+	// Read the YAML
+	bytes, err := ioutil.ReadAll(os.Stdin)
+	if err != nil {
+		return nil, errors.Wrap(err, "Failed to read from stdin")
+	}
+
+	// Parse the YAML
+	config := initData{}
+	err = yaml.Unmarshal(bytes, &config)
+	if err != nil {
+		return nil, errors.Wrap(err, "Failed to parse the preseed")
+	}
+
+	return &config, nil
+}
diff --git a/lxd/main_init_test.go b/lxd/main_init_test.go
deleted file mode 100644
index 9ace94748..000000000
--- a/lxd/main_init_test.go
+++ /dev/null
@@ -1,824 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"path"
-	"path/filepath"
-	"strconv"
-	"testing"
-	"time"
-
-	"github.com/lxc/lxd/client"
-	"github.com/lxc/lxd/lxd/cluster"
-	"github.com/lxc/lxd/lxd/db"
-	"github.com/lxc/lxd/lxd/node"
-	"github.com/lxc/lxd/lxd/util"
-
-	"github.com/lxc/lxd/shared"
-	"github.com/lxc/lxd/shared/api"
-	"github.com/lxc/lxd/shared/cmd"
-	"github.com/lxc/lxd/shared/logging"
-	"github.com/stretchr/testify/suite"
-)
-
-type cmdInitTestSuite struct {
-	lxdTestSuite
-	streams *cmd.MemoryStreams
-	context *cmd.Context
-	args    *Args
-	command *CmdInit
-	client  lxd.ContainerServer
-}
-
-func (suite *cmdInitTestSuite) SetupTest() {
-	logging.Testing(suite.T())
-	suite.lxdTestSuite.SetupTest()
-	suite.streams = cmd.NewMemoryStreams("")
-	suite.context = cmd.NewMemoryContext(suite.streams)
-	suite.args = &Args{
-		NetworkPort:       -1,
-		StorageCreateLoop: -1,
-	}
-	suite.command = &CmdInit{
-		Context:         suite.context,
-		Args:            suite.args,
-		RunningInUserns: false,
-		VarDir:          shared.VarPath(),
-	}
-	client, err := lxd.ConnectLXDUnix(filepath.Join(shared.VarPath(), "unix.socket"), nil)
-	suite.Req.Nil(err)
-	suite.client = client
-}
-
-// If any argument intended for --auto is passed in interactive mode, an
-// error is returned.
-func (suite *cmdInitTestSuite) TestCmdInit_InteractiveWithAutoArgs() {
-	suite.args.NetworkPort = 9999
-	err := suite.command.Run()
-	suite.Req.Equal("Init configuration is only valid with --auto", err.Error())
-}
-
-// If both --auto and --preseed are passed, an error is returned.
-func (suite *cmdInitTestSuite) TestCmdInit_AutoAndPreseedIncompatible() {
-	suite.args.Auto = true
-	suite.args.Preseed = true
-	err := suite.command.Run()
-	suite.Req.Equal("Non-interactive mode supported by only one of --auto or --preseed", err.Error())
-}
-
-// Some arguments can only be passed together with --auto.
-func (suite *cmdInitTestSuite) TestCmdInit_AutoSpecificArgs() {
-	suite.args.StorageBackend = "dir"
-	err := suite.command.Run()
-	suite.Req.Equal("Init configuration is only valid with --auto", err.Error())
-}
-
-// If the YAML preseed data is invalid, an error is returned.
-func (suite *cmdInitTestSuite) TestCmdInit_PreseedInvalidYAML() {
-	suite.args.Preseed = true
-	suite.streams.InputAppend("g at rblEd")
-	err := suite.command.Run()
-	suite.Req.Equal("Invalid preseed YAML content", err.Error())
-}
-
-// Preseed the network address and the trust password.
-func (suite *cmdInitTestSuite) TestCmdInit_PreseedHTTPSAddressAndTrustPassword() {
-	port, err := shared.AllocatePort()
-	suite.Req.Nil(err)
-
-	suite.args.Preseed = true
-	suite.streams.InputAppend(fmt.Sprintf(`config:
-  core.https_address: 127.0.0.1:%d
-  core.trust_password: sekret
-`, port))
-	suite.Req.Nil(suite.command.Run())
-
-	address, err := node.HTTPSAddress(suite.d.db)
-	suite.Req.NoError(err)
-	suite.Req.Equal(fmt.Sprintf("127.0.0.1:%d", port), address)
-	err = suite.d.cluster.Transaction(func(tx *db.ClusterTx) error {
-		config, err := cluster.ConfigLoad(tx)
-		suite.Req.NoError(err)
-		suite.Req.Nil(util.PasswordCheck(config.TrustPassword(), "sekret"))
-		return nil
-	})
-	suite.Req.NoError(err)
-}
-
-// Input network address and trust password interactively.
-func (suite *cmdInitTestSuite) TestCmdInit_InteractiveHTTPSAddressAndTrustPassword() {
-	suite.command.PasswordReader = func(int) ([]byte, error) {
-		return []byte("sekret"), nil
-	}
-	port, err := shared.AllocatePort()
-	suite.Req.Nil(err)
-	answers := &cmdInitAnswers{
-		WantAvailableOverNetwork: true,
-		BindToAddress:            "127.0.0.1",
-		BindToPort:               strconv.Itoa(port),
-	}
-	answers.Render(suite.streams)
-
-	suite.Req.Nil(suite.command.Run())
-
-	address, err := node.HTTPSAddress(suite.d.db)
-	suite.Req.NoError(err)
-	suite.Req.Equal(fmt.Sprintf("127.0.0.1:%d", port), address)
-	err = suite.d.cluster.Transaction(func(tx *db.ClusterTx) error {
-		config, err := cluster.ConfigLoad(tx)
-		suite.Req.NoError(err)
-		suite.Req.Nil(util.PasswordCheck(config.TrustPassword(), "sekret"))
-		return nil
-	})
-	suite.Req.NoError(err)
-}
-
-// Enable clustering interactively.
-func (suite *cmdInitTestSuite) TestCmdInit_InteractiveClustering() {
-	suite.command.PasswordReader = func(int) ([]byte, error) {
-		return []byte("sekret"), nil
-	}
-	port, err := shared.AllocatePort()
-	suite.Req.Nil(err)
-	answers := &cmdInitAnswers{
-		WantClustering: true,
-		ClusterName:    "buzz",
-		ClusterAddress: fmt.Sprintf("127.0.0.1:%d", port),
-	}
-	answers.Render(suite.streams)
-
-	suite.Req.Nil(suite.command.Run())
-	state := suite.d.State()
-	certfile := filepath.Join(state.OS.VarDir, "cluster.crt")
-	suite.Req.True(shared.PathExists(certfile))
-}
-
-// Enable clustering interactively, joining an existing cluser.
-func (suite *cmdInitTestSuite) DISABLED_TestCmdInit_InteractiveClusteringJoin() {
-	leader, cleanup := newDaemon(suite.T())
-	defer cleanup()
-
-	f := clusterFixture{t: suite.T()}
-	f.FormCluster([]*Daemon{leader})
-
-	network := api.NetworksPost{
-		Name:    "mybr",
-		Type:    "bridge",
-		Managed: true,
-	}
-	network.Config = map[string]string{
-		"ipv4.nat": "true",
-	}
-	client := f.ClientUnix(leader)
-	suite.Req.NoError(client.CreateNetwork(network))
-
-	pool := api.StoragePoolsPost{
-		Name:   "mypool",
-		Driver: "dir",
-	}
-	pool.Config = map[string]string{
-		"source": "",
-	}
-	suite.Req.NoError(client.CreateStoragePool(pool))
-
-	suite.command.PasswordReader = func(int) ([]byte, error) {
-		return []byte("sekret"), nil
-	}
-	port, err := shared.AllocatePort()
-	suite.Req.NoError(err)
-	answers := &cmdInitAnswers{
-		WantClustering:           true,
-		ClusterName:              "rusp",
-		ClusterAddress:           fmt.Sprintf("127.0.0.1:%d", port),
-		WantJoinCluster:          true,
-		ClusterTargetNodeAddress: leader.endpoints.NetworkAddress(),
-		ClusterConfirmLosingData: true,
-		ClusterConfig: []string{
-			"", // storage source
-			"", // bridge.external_interfaces
-		},
-	}
-	answers.Render(suite.streams)
-
-	suite.Req.Nil(suite.command.Run())
-	state := suite.d.State()
-	certfile := filepath.Join(state.OS.VarDir, "cluster.crt")
-	suite.Req.True(shared.PathExists(certfile))
-}
-
-// Pass network address and trust password via command line arguments.
-func (suite *cmdInitTestSuite) TestCmdInit_AutoHTTPSAddressAndTrustPassword() {
-	port, err := shared.AllocatePort()
-	suite.Req.Nil(err)
-
-	suite.args.Auto = true
-	suite.args.NetworkAddress = "127.0.0.1"
-	suite.args.NetworkPort = int64(port)
-	suite.args.TrustPassword = "sekret"
-
-	suite.Req.Nil(suite.command.Run())
-
-	address, err := node.HTTPSAddress(suite.d.db)
-	suite.Req.NoError(err)
-	suite.Req.Equal(fmt.Sprintf("127.0.0.1:%d", port), address)
-	err = suite.d.cluster.Transaction(func(tx *db.ClusterTx) error {
-		config, err := cluster.ConfigLoad(tx)
-		suite.Req.NoError(err)
-		suite.Req.Nil(util.PasswordCheck(config.TrustPassword(), "sekret"))
-		return nil
-	})
-	suite.Req.NoError(err)
-}
-
-// The images auto-update interval can be interactively set by simply accepting
-// the answer "yes" to the relevant question.
-func (suite *cmdInitTestSuite) TestCmdInit_ImagesAutoUpdateAnswerYes() {
-	answers := &cmdInitAnswers{
-		WantImageAutoUpdate: true,
-	}
-	answers.Render(suite.streams)
-
-	suite.Req.Nil(suite.command.Run())
-
-	err := suite.d.cluster.Transaction(func(tx *db.ClusterTx) error {
-		config, err := cluster.ConfigLoad(tx)
-		suite.Req.NoError(err)
-		suite.Req.Equal(6*time.Hour, config.AutoUpdateInterval())
-		return nil
-	})
-	suite.Req.NoError(err)
-}
-
-// If the images auto-update interval value is already set to non-zero, it
-// won't be overwritten.
-func (suite *cmdInitTestSuite) TestCmdInit_ImagesAutoUpdateNoOverwrite() {
-	err := suite.d.cluster.Transaction(func(tx *db.ClusterTx) error {
-		config, err := cluster.ConfigLoad(tx)
-		suite.Req.NoError(err)
-		_, err = config.Patch(map[string]interface{}{"images.auto_update_interval": "10"})
-		suite.Req.NoError(err)
-		return nil
-	})
-	suite.Req.Nil(err)
-
-	answers := &cmdInitAnswers{
-		WantImageAutoUpdate: true,
-	}
-	answers.Render(suite.streams)
-
-	suite.Req.Nil(suite.command.Run())
-
-	err = suite.d.cluster.Transaction(func(tx *db.ClusterTx) error {
-		config, err := cluster.ConfigLoad(tx)
-		suite.Req.NoError(err)
-		suite.Req.Equal(10*time.Hour, config.AutoUpdateInterval())
-		return nil
-	})
-	suite.Req.NoError(err)
-}
-
-// If an invalid backend type is passed with --storage-backend, an
-// error is returned.
-func (suite *cmdInitTestSuite) TestCmdInit_AutoWithInvalidBackendType() {
-	suite.args.Auto = true
-	suite.args.StorageBackend = "foo"
-
-	err := suite.command.Run()
-	suite.Req.Equal("The requested backend 'foo' isn't supported by lxd init.", err.Error())
-}
-
-// If an backend type that is not available on the system is passed
-// with --storage-backend, an error is returned.
-func (suite *cmdInitTestSuite) TestCmdInit_AutoWithUnavailableBackendType() {
-	suite.args.Auto = true
-	suite.args.StorageBackend = "zfs"
-	suite.command.RunningInUserns = true // This makes zfs unavailable
-
-	err := suite.command.Run()
-	suite.Req.Equal("The requested backend 'zfs' isn't available on your system (missing tools).", err.Error())
-}
-
-// If --storage-backend is set to "dir", --storage-create-device can't be passed.
-func (suite *cmdInitTestSuite) TestCmdInit_AutoWithDirStorageBackendAndCreateDevice() {
-	suite.args.Auto = true
-	suite.args.StorageBackend = "dir"
-	suite.args.StorageCreateDevice = "/dev/sda4"
-
-	err := suite.command.Run()
-	suite.Req.Equal("None of --storage-pool, --storage-create-device or --storage-create-loop may be used with the 'dir' backend.", err.Error())
-}
-
-// If --storage-backend is set to "dir", and both of --storage-create-device
-// or --storage-create-loop are given, an error is returned.
-func (suite *cmdInitTestSuite) TestCmdInit_AutoWithNonDirBackendAndNoDeviceOrLoop() {
-	suite.args.Auto = true
-	suite.args.StorageBackend = "btrfs"
-	suite.args.StorageCreateDevice = "/dev/sda4"
-	suite.args.StorageCreateLoop = 1
-
-	err := suite.command.Run()
-	suite.Req.Equal("Only one of --storage-create-device or --storage-create-loop can be specified.", err.Error())
-}
-
-// If the user answers "no" to the images auto-update question, the value will
-// be set to 0.
-func (suite *cmdInitTestSuite) TestCmdInit_ImagesAutoUpdateAnswerNo() {
-	answers := &cmdInitAnswers{
-		WantImageAutoUpdate: false,
-	}
-	answers.Render(suite.streams)
-
-	suite.Req.Nil(suite.command.Run())
-
-	err := suite.d.cluster.Transaction(func(tx *db.ClusterTx) error {
-		config, err := cluster.ConfigLoad(tx)
-		suite.Req.NoError(err)
-		suite.Req.Equal(time.Duration(0), config.AutoUpdateInterval())
-		return nil
-	})
-	suite.Req.NoError(err)
-}
-
-// If the user answers "no" to the images auto-update question, the value will
-// be set to 0, even it was already set to some value.
-func (suite *cmdInitTestSuite) TestCmdInit_ImagesAutoUpdateOverwriteIfZero() {
-	err := suite.d.cluster.Transaction(func(tx *db.ClusterTx) error {
-		config, err := cluster.ConfigLoad(tx)
-		suite.Req.NoError(err)
-		_, err = config.Patch(map[string]interface{}{"images.auto_update_interval": "10"})
-		suite.Req.NoError(err)
-		return nil
-	})
-	suite.Req.Nil(err)
-
-	answers := &cmdInitAnswers{
-		WantImageAutoUpdate: false,
-	}
-	answers.Render(suite.streams)
-
-	suite.Req.Nil(suite.command.Run())
-
-	err = suite.d.cluster.Transaction(func(tx *db.ClusterTx) error {
-		config, err := cluster.ConfigLoad(tx)
-		suite.Req.NoError(err)
-		suite.Req.Equal(time.Duration(0), config.AutoUpdateInterval())
-		return nil
-	})
-	suite.Req.NoError(err)
-}
-
-// Preseed the image auto-update interval.
-func (suite *cmdInitTestSuite) TestCmdInit_ImagesAutoUpdatePreseed() {
-	suite.args.Preseed = true
-	suite.streams.InputAppend(`config:
-  images.auto_update_interval: 15
-`)
-	suite.Req.Nil(suite.command.Run())
-
-	err := suite.d.cluster.Transaction(func(tx *db.ClusterTx) error {
-		config, err := cluster.ConfigLoad(tx)
-		suite.Req.NoError(err)
-		suite.Req.Equal(15*time.Hour, config.AutoUpdateInterval())
-		return nil
-	})
-	suite.Req.NoError(err)
-}
-
-// If --storage-backend is set to "dir" a storage pool is created.
-func (suite *cmdInitTestSuite) TestCmdInit_StoragePoolAuto() {
-	// Clear the storage pool created by default by the test suite
-	profile, _, err := suite.client.GetProfile("default")
-	suite.Req.Nil(err)
-	profileData := profile.Writable()
-	delete(profileData.Devices, "root")
-	err = suite.client.UpdateProfile("default", profileData, "")
-	suite.Req.Nil(err)
-	suite.Req.Nil(suite.client.DeleteStoragePool(lxdTestSuiteDefaultStoragePool))
-
-	suite.args.Auto = true
-	suite.args.StorageBackend = "dir"
-
-	suite.Req.Nil(suite.command.Run())
-	pool, _, err := suite.client.GetStoragePool("default")
-	suite.Req.Nil(err)
-	suite.Req.Equal("dir", pool.Driver)
-	suite.Req.Equal(path.Join(suite.tmpdir, "storage-pools", "default"), pool.Config["source"])
-}
-
-// Preseed a new storage pool.
-func (suite *cmdInitTestSuite) TestCmdInit_StoragePoolPreseed() {
-	suite.args.Preseed = true
-	suite.streams.InputAppend(`storage_pools:
-- name: foo
-  driver: dir
-  config:
-    source: ""
-`)
-
-	suite.Req.Nil(suite.command.Run())
-
-	pool, _, err := suite.client.GetStoragePool("foo")
-	suite.Req.Nil(err)
-	suite.Req.Equal("dir", pool.Driver)
-	suite.Req.Equal(path.Join(suite.tmpdir, "storage-pools", "foo"), pool.Config["source"])
-}
-
-// If an error occurs when creating a new storage pool, all new pools created
-// so far get deleted. Any server config that got applied, gets reset too.
-func (suite *cmdInitTestSuite) TestCmdInit_StoragePoolCreateRevert() {
-	suite.args.Preseed = true
-	suite.streams.InputAppend(`config:
-images.auto_update_interval: 15
-storage_pools:
-- name: first
-  driver: dir
-  config:
-    source: ""
-- name: second
-  driver: dir
-  config:
-    boom: garbage
-`)
-
-	err := suite.command.Run()
-	suite.Req.Equal("Invalid storage pool configuration key: boom", err.Error())
-
-	_, _, err = suite.client.GetStoragePool("first")
-	suite.Req.Equal("not found", err.Error())
-
-	_, _, err = suite.client.GetStoragePool("second")
-	suite.Req.Equal("not found", err.Error())
-
-	interval, err := cluster.ConfigGetInt64(suite.d.cluster, "images.auto_update_interval")
-	suite.Req.NoError(err)
-	suite.Req.NotEqual(int64(15), interval)
-}
-
-// Updating a storage pool via preseed will fail, since it's not supported
-// by the API.
-func (suite *cmdInitTestSuite) TestCmdInit_StoragePoolPreseedUpdate() {
-	post := api.StoragePoolsPost{
-		Name:   "egg",
-		Driver: "dir",
-	}
-	post.Config = map[string]string{
-		"source": "",
-	}
-	err := suite.client.CreateStoragePool(post)
-	suite.Req.Nil(err)
-
-	suite.args.Preseed = true
-	suite.streams.InputAppend(`storage_pools:
-- name: egg
-  driver: dir
-  config:
-    source: /egg
-`)
-
-	err = suite.command.Run()
-	suite.Req.Error(err)
-}
-
-// It's possible to configure a network bridge interactively.
-func (suite *cmdInitTestSuite) TestCmdInit_NetworkInteractive() {
-	answers := &cmdInitAnswers{
-		WantNetworkBridge: true,
-		BridgeName:        "foo",
-		BridgeIPv4:        "auto",
-		BridgeIPv6:        "auto",
-	}
-	answers.Render(suite.streams)
-
-	suite.Req.Nil(suite.command.Run())
-
-	network, _, err := suite.client.GetNetwork("foo")
-	suite.Req.Nil(err)
-	suite.Req.Equal("bridge", network.Type)
-	suite.Req.Nil(networkValidAddressCIDRV4(network.Config["ipv4.address"]))
-	suite.Req.Nil(networkValidAddressCIDRV6(network.Config["ipv6.address"]))
-}
-
-// Preseed a network of type bridge.
-func (suite *cmdInitTestSuite) TestCmdInit_NetworkPreseed() {
-	suite.args.Preseed = true
-	suite.streams.InputAppend(`networks:
-- name: bar
-  type: bridge
-  config:
-    ipv4.address: 10.48.159.1/24
-    ipv4.nat: true
-    ipv6.address: none
-`)
-
-	suite.Req.Nil(suite.command.Run())
-
-	network, _, err := suite.client.GetNetwork("bar")
-	suite.Req.Nil(err)
-	suite.Req.Equal("bridge", network.Type)
-	suite.Req.Equal("10.48.159.1/24", network.Config["ipv4.address"])
-	suite.Req.Equal("true", network.Config["ipv4.nat"])
-	suite.Req.Equal("none", network.Config["ipv6.address"])
-}
-
-// Update a network via preseed.
-func (suite *cmdInitTestSuite) TestCmdInit_NetworkPreseedUpdate() {
-	post := api.NetworksPost{
-		Name: "egg",
-	}
-	post.Config = map[string]string{
-		"ipv4.address": "10.48.159.1/24",
-		"ipv4.nat":     "true",
-		"ipv6.address": "none",
-	}
-	err := suite.client.CreateNetwork(post)
-	suite.Req.Nil(err)
-
-	suite.args.Preseed = true
-	suite.streams.InputAppend(`networks:
-- name: egg
-  type: bridge
-  config:
-    ipv4.address: none
-    ipv4.nat: false
-    ipv6.address: auto
-`)
-
-	suite.Req.Nil(suite.command.Run())
-
-	network, _, err := suite.client.GetNetwork("egg")
-	suite.Req.Nil(err)
-	suite.Req.Equal("bridge", network.Type)
-	suite.Req.Equal("none", network.Config["ipv4.address"])
-	suite.Req.Equal("false", network.Config["ipv4.nat"])
-	suite.Req.Nil(networkValidAddressCIDRV6(network.Config["ipv6.address"]))
-}
-
-// Updating a network via preseed and changing it's type to something else
-// than "bridge" results in an error.
-func (suite *cmdInitTestSuite) TestCmdInit_NetworkPreseedUpdateNonBridge() {
-	post := api.NetworksPost{
-		Name: "baz",
-	}
-	post.Config = map[string]string{
-		"ipv4.address": "10.48.159.1/24",
-		"ipv4.nat":     "true",
-		"ipv6.address": "none",
-	}
-	err := suite.client.CreateNetwork(post)
-	suite.Req.Nil(err)
-
-	suite.args.Preseed = true
-	suite.streams.InputAppend(`networks:
-- name: baz
-  type: physical
-  config:
-    ipv4.address: 10.48.159.1/24
-    ipv4.nat: true
-    ipv6.address: none
-`)
-
-	err = suite.command.Run()
-	suite.Req.Equal("Only 'bridge' type networks are supported", err.Error())
-}
-
-// If an error occurs when creating a new network, all new networks created
-// so far get deleted.
-func (suite *cmdInitTestSuite) TestCmdInit_NetworkCreateRevert() {
-	suite.args.Preseed = true
-	suite.streams.InputAppend(`networks:
-- name: first
-  type: bridge
-  config:
-    ipv4.address: 10.48.159.1/24
-    ipv4.nat: true
-    ipv6.address: none
-- name: second
-  type: bridge
-  config:
-    boom: garbage
-`)
-
-	err := suite.command.Run()
-	suite.Req.Equal("Invalid network configuration key: boom", err.Error())
-
-	_, _, err = suite.client.GetNetwork("first")
-	suite.Req.Equal("not found", err.Error())
-
-	_, _, err = suite.client.GetNetwork("second")
-	suite.Req.Equal("not found", err.Error())
-}
-
-// Preseed a new profile.
-func (suite *cmdInitTestSuite) TestCmdInit_ProfilesPreseed() {
-	suite.args.Preseed = true
-	suite.streams.InputAppend(`profiles:
-- name: bar
-  description: "Bar profile"
-  config:
-    limits.memory: 2GB
-  devices:
-    data:
-      path: /srv/data/
-      source: /some/data
-      type: disk
-`)
-
-	suite.Req.Nil(suite.command.Run())
-
-	profile, _, err := suite.client.GetProfile("bar")
-	suite.Req.Nil(err)
-	suite.Req.Equal("Bar profile", profile.Description)
-	suite.Req.Equal("2GB", profile.Config["limits.memory"])
-	suite.Req.Equal("/srv/data/", profile.Devices["data"]["path"])
-	suite.Req.Equal("/some/data", profile.Devices["data"]["source"])
-	suite.Req.Equal("disk", profile.Devices["data"]["type"])
-}
-
-// If an error occurs while creating a new profile, all other profiles
-// created in by the preseeded YAML get deleted.
-func (suite *cmdInitTestSuite) TestCmdInit_ProfilesCreateRevert() {
-	suite.args.Preseed = true
-	suite.streams.InputAppend(`profiles:
-- name: first
-  description: "First profile"
-  config:
-    limits.memory: 2GB
-  devices:
-    data:
-      path: /srv/data/
-      source: /some/data
-      type: disk
-- name: second
-  description: "Second profile"
-  config:
-    boom: garbage
-  devices:
-    data:
-      path: /srv/data/
-      source: /some/data
-      type: disk
-`)
-
-	err := suite.command.Run()
-	suite.Req.Equal("Unknown configuration key: boom", err.Error())
-	_, _, err = suite.client.GetProfile("first")
-	suite.Req.Equal("not found", err.Error())
-
-	_, _, err = suite.client.GetProfile("second")
-	suite.Req.Equal("not found", err.Error())
-}
-
-// If an error occurs while creating a new profile, all other profiles
-// that have been updated in by the preseeded YAML get reverted.
-func (suite *cmdInitTestSuite) TestCmdInit_ProfilesUpdateRevert() {
-	post := api.ProfilesPost{
-		Name: "first",
-	}
-	post.Description = "First profile profile"
-	post.Config = map[string]string{
-		"limits.memory": "2GB",
-	}
-	post.Devices = map[string]map[string]string{
-		"data": {
-			"path":   "/srv/data/",
-			"source": "/some/data",
-			"type":   "disk",
-		},
-	}
-	err := suite.client.CreateProfile(post)
-
-	suite.Req.Nil(err)
-
-	suite.args.Preseed = true
-	suite.streams.InputAppend(`profiles:
-- name: first
-  description: "First profile"
-  config:
-    limits.memory: 4GB
-  devices:
-    data:
-      path: /srv/data/
-      source: /some/data
-      type: disk
-- name: second
-  description: "Second profile"
-  config:
-    boom: garbage
-  devices:
-    data:
-      path: /srv/data/
-      source: /some/data
-      type: disk
-`)
-
-	err = suite.command.Run()
-	suite.Req.Equal("Unknown configuration key: boom", err.Error())
-
-	profile, _, err := suite.client.GetProfile("first")
-	suite.Req.Nil(err)
-	suite.Req.Equal("2GB", profile.Config["limits.memory"])
-
-	_, _, err = suite.client.GetProfile("second")
-	suite.Req.Equal("not found", err.Error())
-}
-
-// Update a profile via preseed.
-func (suite *cmdInitTestSuite) TestCmdInit_ProfilesPreseedUpdate() {
-	post := api.ProfilesPost{
-		Name: "egg",
-	}
-	post.Description = "Egg profile"
-	post.Config = map[string]string{
-		"limits.memory": "2GB",
-	}
-	post.Devices = map[string]map[string]string{
-		"data": {
-			"path":   "/srv/data/",
-			"source": "/some/data",
-			"type":   "disk",
-		},
-	}
-	err := suite.client.CreateProfile(post)
-	suite.Req.Nil(err)
-
-	suite.args.Preseed = true
-	suite.streams.InputAppend(`profiles:
-- name: egg
-  description: "Egg profile enhanced"
-  config:
-    limits.memory: 4GB
-  devices:
-    data:
-      path: /srv/more/data/
-      source: /some/data
-      type: disk
-`)
-
-	suite.Req.Nil(suite.command.Run())
-
-	profile, _, err := suite.client.GetProfile("egg")
-	suite.Req.Nil(err)
-	suite.Req.Equal("Egg profile enhanced", profile.Description)
-	suite.Req.Equal("4GB", profile.Config["limits.memory"])
-	suite.Req.Equal("/srv/more/data/", profile.Devices["data"]["path"])
-	suite.Req.Equal("/some/data", profile.Devices["data"]["source"])
-	suite.Req.Equal("disk", profile.Devices["data"]["type"])
-}
-
-// Convenience for building the input text a user would enter for a certain
-// sequence of answers.
-type cmdInitAnswers struct {
-	WantClustering           bool
-	ClusterName              string
-	ClusterAddress           string
-	WantJoinCluster          bool
-	ClusterTargetNodeAddress string
-	ClusterConfirmLosingData bool
-	ClusterConfig            []string
-	WantStoragePool          bool
-	WantAvailableOverNetwork bool
-	BindToAddress            string
-	BindToPort               string
-	WantImageAutoUpdate      bool
-	WantNetworkBridge        bool
-	BridgeName               string
-	BridgeIPv4               string
-	BridgeIPv6               string
-}
-
-// Render the input text the user would type for the desired answers, populating
-// the stdin of the given streams.
-func (answers *cmdInitAnswers) Render(streams *cmd.MemoryStreams) {
-	streams.InputAppendBoolAnswer(answers.WantClustering)
-	if answers.WantClustering {
-		streams.InputAppendLine(answers.ClusterName)
-		streams.InputAppendLine(answers.ClusterAddress)
-		streams.InputAppendBoolAnswer(answers.WantJoinCluster)
-		if answers.WantJoinCluster {
-			streams.InputAppendLine(answers.ClusterTargetNodeAddress)
-			streams.InputAppendBoolAnswer(answers.ClusterConfirmLosingData)
-			for _, value := range answers.ClusterConfig {
-				streams.InputAppendLine(value)
-			}
-		}
-	}
-	streams.InputAppendBoolAnswer(answers.WantStoragePool)
-	if !answers.WantClustering {
-		streams.InputAppendBoolAnswer(answers.WantAvailableOverNetwork)
-	}
-	if answers.WantAvailableOverNetwork {
-		streams.InputAppendLine(answers.BindToAddress)
-		streams.InputAppendLine(answers.BindToPort)
-	}
-	streams.InputAppendBoolAnswer(answers.WantImageAutoUpdate)
-	streams.InputAppendBoolAnswer(answers.WantNetworkBridge)
-	if answers.WantNetworkBridge {
-		streams.InputAppendLine(answers.BridgeName)
-		streams.InputAppendLine(answers.BridgeIPv4)
-		streams.InputAppendLine(answers.BridgeIPv6)
-	}
-}
-
-func TestCmdInitTestSuite(t *testing.T) {
-	suite.Run(t, new(cmdInitTestSuite))
-}
diff --git a/shared/cmd/ask.go b/shared/cmd/ask.go
new file mode 100644
index 000000000..503d75c99
--- /dev/null
+++ b/shared/cmd/ask.go
@@ -0,0 +1,141 @@
+package cmd
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+	"strconv"
+	"strings"
+
+	"golang.org/x/crypto/ssh/terminal"
+
+	"github.com/lxc/lxd/shared"
+)
+
+var stdin = bufio.NewReader(os.Stdin)
+
+// AskBool asks a question an expect a yes/no answer.
+func AskBool(question string, defaultAnswer string) bool {
+	for {
+		answer := askQuestion(question, defaultAnswer)
+
+		if shared.StringInSlice(strings.ToLower(answer), []string{"yes", "y"}) {
+			return true
+		} else if shared.StringInSlice(strings.ToLower(answer), []string{"no", "n"}) {
+			return false
+		}
+
+		invalidInput()
+	}
+}
+
+// AskChoice asks the user to select between a set of choices
+func AskChoice(question string, choices []string, defaultAnswer string) string {
+	for {
+		answer := askQuestion(question, defaultAnswer)
+
+		if shared.StringInSlice(answer, choices) {
+			return answer
+		}
+
+		invalidInput()
+	}
+}
+
+// AskInt asks the user to enter an integer between a min and max value
+func AskInt(question string, min int64, max int64, defaultAnswer string) int64 {
+	for {
+		answer := askQuestion(question, defaultAnswer)
+
+		result, err := strconv.ParseInt(answer, 10, 64)
+
+		if err == nil && (min == -1 || result >= min) && (max == -1 || result <= max) {
+			return result
+		}
+
+		invalidInput()
+	}
+}
+
+// AskString asks the user to enter a string, which optionally
+// conforms to a validation function.
+func AskString(question string, defaultAnswer string, validate func(string) error) string {
+	for {
+		answer := askQuestion(question, defaultAnswer)
+
+		if validate != nil {
+			error := validate(answer)
+			if error != nil {
+				fmt.Fprintf(os.Stderr, "Invalid input: %s\n\n", error)
+				continue
+			}
+
+			return answer
+		}
+
+		if len(answer) != 0 {
+			return answer
+		}
+
+		invalidInput()
+	}
+}
+
+// AskPassword asks the user to enter a password.
+func AskPassword(question string) string {
+	for {
+		fmt.Printf(question)
+
+		pwd, _ := terminal.ReadPassword(0)
+		fmt.Println("")
+		inFirst := string(pwd)
+		inFirst = strings.TrimSuffix(inFirst, "\n")
+
+		fmt.Printf("Again: ")
+		pwd, _ = terminal.ReadPassword(0)
+		fmt.Println("")
+		inSecond := string(pwd)
+		inSecond = strings.TrimSuffix(inSecond, "\n")
+
+		if inFirst == inSecond {
+			return inFirst
+		}
+
+		invalidInput()
+	}
+}
+
+// AskPasswordOnce asks the user to enter a password.
+//
+// It's the same as AskPassword, but it won't ask to enter it again.
+func AskPasswordOnce(question string) string {
+	fmt.Printf(question)
+	pwd, _ := terminal.ReadPassword(0)
+	fmt.Println("")
+
+	return string(pwd)
+}
+
+// Ask a question on the output stream and read the answer from the input stream
+func askQuestion(question, defaultAnswer string) string {
+	fmt.Printf(question)
+
+	return readAnswer(defaultAnswer)
+}
+
+// Read the user's answer from the input stream, trimming newline and providing a default.
+func readAnswer(defaultAnswer string) string {
+	answer, _ := stdin.ReadString('\n')
+	answer = strings.TrimSuffix(answer, "\n")
+	answer = strings.TrimSpace(answer)
+	if answer == "" {
+		answer = defaultAnswer
+	}
+
+	return answer
+}
+
+// Print an invalid input message on the error stream
+func invalidInput() {
+	fmt.Fprintf(os.Stderr, "Invalid input, try again.\n\n")
+}
diff --git a/shared/cmd/context.go b/shared/cmd/context.go
deleted file mode 100644
index e8d7d6104..000000000
--- a/shared/cmd/context.go
+++ /dev/null
@@ -1,181 +0,0 @@
-package cmd
-
-import (
-	"bufio"
-	"fmt"
-	"io"
-	"io/ioutil"
-	"os"
-	"strconv"
-	"strings"
-
-	"gopkg.in/yaml.v2"
-
-	"github.com/lxc/lxd/shared"
-)
-
-// Context captures the environment the sub-command is being run in,
-// such as in/out/err streams and command line arguments.
-type Context struct {
-	stdin  *bufio.Reader
-	stdout io.Writer
-	stderr io.Writer
-}
-
-// DefaultContext returns a new Context connected the stdin, stdout and stderr
-// streams.
-func DefaultContext() *Context {
-	return NewContext(os.Stdin, os.Stderr, os.Stdout)
-}
-
-// NewContext creates a new command context with the given parameters.
-func NewContext(stdin io.Reader, stdout, stderr io.Writer) *Context {
-	return &Context{
-		stdin:  bufio.NewReader(stdin),
-		stdout: stdout,
-		stderr: stderr,
-	}
-}
-
-// Output prints a message on standard output.
-func (c *Context) Output(format string, a ...interface{}) {
-	fmt.Fprintf(c.stdout, format, a...)
-}
-
-// Error prints a message on standard error.
-func (c *Context) Error(format string, a ...interface{}) {
-	fmt.Fprintf(c.stderr, format, a...)
-}
-
-// AskBool asks a question an expect a yes/no answer.
-func (c *Context) AskBool(question string, defaultAnswer string) bool {
-	for {
-		answer := c.askQuestion(question, defaultAnswer)
-
-		if shared.StringInSlice(strings.ToLower(answer), []string{"yes", "y"}) {
-			return true
-		} else if shared.StringInSlice(strings.ToLower(answer), []string{"no", "n"}) {
-			return false
-		}
-
-		c.invalidInput()
-	}
-}
-
-// AskChoice asks the user to select between a set of choices
-func (c *Context) AskChoice(question string, choices []string, defaultAnswer string) string {
-	for {
-		answer := c.askQuestion(question, defaultAnswer)
-
-		if shared.StringInSlice(answer, choices) {
-			return answer
-		}
-
-		c.invalidInput()
-	}
-}
-
-// AskInt asks the user to enter an integer between a min and max value
-func (c *Context) AskInt(question string, min int64, max int64, defaultAnswer string) int64 {
-	for {
-		answer := c.askQuestion(question, defaultAnswer)
-
-		result, err := strconv.ParseInt(answer, 10, 64)
-
-		if err == nil && (min == -1 || result >= min) && (max == -1 || result <= max) {
-			return result
-		}
-
-		c.invalidInput()
-	}
-}
-
-// AskString asks the user to enter a string, which optionally
-// conforms to a validation function.
-func (c *Context) AskString(question string, defaultAnswer string, validate func(string) error) string {
-	for {
-		answer := c.askQuestion(question, defaultAnswer)
-
-		if validate != nil {
-			error := validate(answer)
-			if error != nil {
-				fmt.Fprintf(c.stderr, "Invalid input: %s\n\n", error)
-				continue
-			}
-			return answer
-		}
-		if len(answer) != 0 {
-			return answer
-		}
-
-		c.invalidInput()
-	}
-}
-
-// AskPassword asks the user to enter a password. The reader function used to
-// read the password without echoing characters must be passed (usually
-// terminal.ReadPassword from golang.org/x/crypto/ssh/terminal).
-func (c *Context) AskPassword(question string, reader func(int) ([]byte, error)) string {
-	for {
-		fmt.Fprintf(c.stdout, question)
-
-		pwd, _ := reader(0)
-		fmt.Fprintf(c.stdout, "\n")
-		inFirst := string(pwd)
-		inFirst = strings.TrimSuffix(inFirst, "\n")
-
-		fmt.Fprintf(c.stdout, "Again: ")
-		pwd, _ = reader(0)
-		fmt.Fprintf(c.stdout, "\n")
-		inSecond := string(pwd)
-		inSecond = strings.TrimSuffix(inSecond, "\n")
-
-		if inFirst == inSecond {
-			return inFirst
-		}
-
-		c.invalidInput()
-	}
-}
-
-// AskPasswordOnce asks the user to enter a password.
-//
-// It's the same as AskPassword, but it won't ask to enter it again.
-func (c *Context) AskPasswordOnce(question string, reader func(int) ([]byte, error)) string {
-	fmt.Fprintf(c.stdout, question)
-	pwd, _ := reader(0)
-	fmt.Fprintf(c.stdout, "\n")
-	return string(pwd)
-}
-
-// InputYAML treats stdin as YAML content and returns the unmarshalled
-// structure
-func (c *Context) InputYAML(out interface{}) error {
-	bytes, err := ioutil.ReadAll(c.stdin)
-	if err != nil {
-		return err
-	}
-	return yaml.Unmarshal(bytes, out)
-}
-
-// Ask a question on the output stream and read the answer from the input stream
-func (c *Context) askQuestion(question, defaultAnswer string) string {
-	fmt.Fprintf(c.stdout, question)
-	return c.readAnswer(defaultAnswer)
-}
-
-// Print an invalid input message on the error stream
-func (c *Context) invalidInput() {
-	fmt.Fprintf(c.stderr, "Invalid input, try again.\n\n")
-}
-
-// Read the user's answer from the input stream, trimming newline and providing a default.
-func (c *Context) readAnswer(defaultAnswer string) string {
-	answer, _ := c.stdin.ReadString('\n')
-	answer = strings.TrimSuffix(answer, "\n")
-	answer = strings.TrimSpace(answer)
-	if answer == "" {
-		answer = defaultAnswer
-	}
-	return answer
-}
diff --git a/shared/cmd/context_test.go b/shared/cmd/context_test.go
deleted file mode 100644
index 99cad710c..000000000
--- a/shared/cmd/context_test.go
+++ /dev/null
@@ -1,195 +0,0 @@
-package cmd_test
-
-import (
-	"fmt"
-	"testing"
-
-	"github.com/lxc/lxd/shared/cmd"
-	"github.com/stretchr/testify/assert"
-)
-
-// AssertOutEqual checks that the given text matches the the out stream.
-func AssertOutEqual(t *testing.T, stream *cmd.MemoryStreams, expected string) {
-	assert.Equal(t, expected, stream.Out(), "Unexpected output stream")
-}
-
-// AssertErrEqual checks that the given text matches the the err stream.
-func AssertErrEqual(t *testing.T, stream *cmd.MemoryStreams, expected string) {
-	assert.Equal(t, expected, stream.Err(), "Unexpected error stream")
-}
-
-// Output prints the given message on standard output
-func TestOutput(t *testing.T) {
-	streams := cmd.NewMemoryStreams("")
-	context := cmd.NewMemoryContext(streams)
-	context.Output("Hello %s", "world")
-	AssertOutEqual(t, streams, "Hello world")
-}
-
-// AskBool returns a boolean result depending on the user input.
-func TestAskBool(t *testing.T) {
-	cases := []struct {
-		question      string
-		defaultAnswer string
-		output        string
-		error         string
-		input         string
-		result        bool
-	}{
-		{"Do you code?", "yes", "Do you code?", "", "\n", true},
-		{"Do you code?", "yes", "Do you code?", "", "yes\n", true},
-		{"Do you code?", "yes", "Do you code?", "", "y\n", true},
-		{"Do you code?", "yes", "Do you code?", "", "no\n", false},
-		{"Do you code?", "yes", "Do you code?", "", "n\n", false},
-		{"Do you code?", "yes", "Do you code?Do you code?", "Invalid input, try again.\n\n", "foo\nyes\n", true},
-	}
-	for _, c := range cases {
-		streams := cmd.NewMemoryStreams(c.input)
-		context := cmd.NewMemoryContext(streams)
-		result := context.AskBool(c.question, c.defaultAnswer)
-
-		assert.Equal(t, c.result, result, "Unexpected answer result")
-		AssertOutEqual(t, streams, c.output)
-		AssertErrEqual(t, streams, c.error)
-	}
-}
-
-// AskChoice returns one of the given choices
-func TestAskChoice(t *testing.T) {
-	cases := []struct {
-		question      string
-		choices       []string
-		defaultAnswer string
-		output        string
-		error         string
-		input         string
-		result        string
-	}{
-		{"Best food?", []string{"pizza", "rice"}, "rice", "Best food?", "", "\n", "rice"},
-		{"Best food?", []string{"pizza", "rice"}, "rice", "Best food?", "", "pizza\n", "pizza"},
-		{"Best food?", []string{"pizza", "rice"}, "rice", "Best food?Best food?", "Invalid input, try again.\n\n", "foo\npizza\n", "pizza"},
-	}
-	for _, c := range cases {
-		streams := cmd.NewMemoryStreams(c.input)
-		context := cmd.NewMemoryContext(streams)
-		result := context.AskChoice(c.question, c.choices, c.defaultAnswer)
-
-		assert.Equal(t, c.result, result, "Unexpected answer result")
-		AssertOutEqual(t, streams, c.output)
-		AssertErrEqual(t, streams, c.error)
-	}
-}
-
-// AskInt returns an integer within the given bounds
-func TestAskInt(t *testing.T) {
-	cases := []struct {
-		question      string
-		min           int64
-		max           int64
-		defaultAnswer string
-		output        string
-		error         string
-		input         string
-		result        int64
-	}{
-		{"Age?", 0, 100, "30", "Age?", "", "\n", 30},
-		{"Age?", 0, 100, "30", "Age?", "", "40\n", 40},
-		{"Age?", 0, 100, "30", "Age?Age?", "Invalid input, try again.\n\n", "foo\n40\n", 40},
-		{"Age?", 18, 65, "30", "Age?Age?", "Invalid input, try again.\n\n", "10\n30\n", 30},
-		{"Age?", 18, 65, "30", "Age?Age?", "Invalid input, try again.\n\n", "70\n30\n", 30},
-		{"Age?", 0, -1, "30", "Age?", "", "120\n", 120},
-	}
-	for _, c := range cases {
-		streams := cmd.NewMemoryStreams(c.input)
-		context := cmd.NewMemoryContext(streams)
-		result := context.AskInt(c.question, c.min, c.max, c.defaultAnswer)
-
-		assert.Equal(t, c.result, result, "Unexpected answer result")
-		AssertOutEqual(t, streams, c.output)
-		AssertErrEqual(t, streams, c.error)
-	}
-}
-
-// AskString returns a string conforming the validation function.
-func TestAskString(t *testing.T) {
-	cases := []struct {
-		question      string
-		defaultAnswer string
-		validate      func(string) error
-		output        string
-		error         string
-		input         string
-		result        string
-	}{
-		{"Name?", "Joe", nil, "Name?", "", "\n", "Joe"},
-		{"Name?", "Joe", nil, "Name?", "", "John\n", "John"},
-		{"Name?", "Joe", func(s string) error {
-			if s[0] != 'J' {
-				return fmt.Errorf("ugly name")
-			}
-			return nil
-		}, "Name?Name?", "Invalid input: ugly name\n\n", "Ted\nJohn", "John"},
-		{"Name?", "", func(string) error { return nil }, "Name?", "", "\n", ""},
-	}
-	for _, c := range cases {
-		streams := cmd.NewMemoryStreams(c.input)
-		context := cmd.NewMemoryContext(streams)
-		result := context.AskString(c.question, c.defaultAnswer, c.validate)
-
-		assert.Equal(t, c.result, result, "Unexpected answer result")
-		AssertOutEqual(t, streams, c.output)
-		AssertErrEqual(t, streams, c.error)
-	}
-}
-
-// AskPassword returns the password entered twice by the user.
-func TestAskPassword(t *testing.T) {
-	cases := []struct {
-		question string
-		reader   func(int) ([]byte, error)
-		output   string
-		error    string
-		result   string
-	}{
-		{"Pass?", func(int) ([]byte, error) {
-			return []byte("pwd"), nil
-		}, "Pass?\nAgain: \n", "", "pwd"},
-	}
-	for _, c := range cases {
-		streams := cmd.NewMemoryStreams("")
-		context := cmd.NewMemoryContext(streams)
-		result := context.AskPassword(c.question, c.reader)
-
-		assert.Equal(t, c.result, result, "Unexpected answer result")
-		AssertOutEqual(t, streams, c.output)
-		AssertErrEqual(t, streams, c.error)
-	}
-}
-
-// AskPasswordOnce returns the password entered once by the user.
-func TestAskPasswordOnce(t *testing.T) {
-	streams := cmd.NewMemoryStreams("")
-	context := cmd.NewMemoryContext(streams)
-
-	reader := func(int) ([]byte, error) {
-		return []byte("pwd"), nil
-	}
-
-	result := context.AskPassword("Pass?", reader)
-
-	assert.Equal(t, "pwd", result, "Unexpected answer result")
-}
-
-// InputYAML parses the YAML content passed via stdin.
-func TestInputYAML(t *testing.T) {
-	streams := cmd.NewMemoryStreams("field: foo")
-	context := cmd.NewMemoryContext(streams)
-
-	type Schema struct {
-		Field string
-	}
-	schema := Schema{}
-
-	assert.Nil(t, context.InputYAML(&schema))
-	assert.Equal(t, "foo", schema.Field, "Unexpected field value")
-}
diff --git a/shared/cmd/doc.go b/shared/cmd/doc.go
deleted file mode 100644
index 02dab7258..000000000
--- a/shared/cmd/doc.go
+++ /dev/null
@@ -1,11 +0,0 @@
-/*
-
-The package cmd implements a simple abstraction around a "sub-command" for
-a main executable (e.g. "lxd init", where "init" is the sub-command).
-
-It is designed to make unit-testing easier, since OS-specific parts like
-standard in/out can be set in tests.
-
-*/
-
-package cmd
diff --git a/shared/cmd/parser.go b/shared/cmd/parser.go
deleted file mode 100644
index ce71eaeeb..000000000
--- a/shared/cmd/parser.go
+++ /dev/null
@@ -1,142 +0,0 @@
-package cmd
-
-import (
-	"reflect"
-	"strings"
-	"unsafe"
-
-	"github.com/lxc/lxd/shared/gnuflag"
-)
-
-// Parser for command line arguments.
-type Parser struct {
-	Context      *Context
-	UsageMessage string
-	ExitOnError  bool
-}
-
-// NewParser returns a Parser connected to the given I/O context and printing
-// the given usage message when '--help' or '-h' are passed.
-func NewParser(context *Context, usage string) *Parser {
-	return &Parser{
-		Context:      context,
-		UsageMessage: usage,
-		ExitOnError:  true,
-	}
-}
-
-// Parse a command line populating the given args object accordingly.
-//
-// The command line format is expected to be:
-//
-// <cmd> [subcmd [params]] [flags] [-- [extra]]
-//
-// The args object may have Subcommand, Params and Extra attributes
-// (respectively of type string, []string and []string), which will be
-// populated with the subcommand, its params and any extra argument (if
-// present).
-//
-// The type of the args object must have one attribute for each supported
-// command line flag, annotated with a tag like `flag:"<name>"`, where <name>
-// is the name of the command line flag.
-//
-// In case of parsing error (e.g. unknown command line flag) the default
-// behavior is to call os.Exit() with a non-zero value. This can be disabled by
-// setting the ExitOnError attribute to false, in which case the error will be
-// returned.
-func (p *Parser) Parse(line []string, args interface{}) error {
-	val := reflect.ValueOf(args).Elem()
-
-	if err := p.parseFlags(line, val); err != nil {
-		return err
-	}
-
-	p.parseRest(line, val)
-
-	return nil
-}
-
-// Populate the given FlagSet by introspecting the given object, adding a new
-// flag variable for each annotated attribute.
-func (p *Parser) parseFlags(line []string, val reflect.Value) error {
-	mode := gnuflag.ContinueOnError
-	if p.ExitOnError {
-		mode = gnuflag.ExitOnError
-	}
-
-	flags := gnuflag.NewFlagSet(line[0], mode)
-	flags.SetOutput(p.Context.stderr)
-
-	if p.UsageMessage != "" {
-		// Since usage will be printed only if "-h" or "--help" are
-		// explicitly set in the command line, use stdout for it.
-		flags.Usage = func() {
-			p.Context.Output(p.UsageMessage)
-		}
-	}
-
-	typ := val.Type()
-	for i := 0; i < typ.NumField(); i++ {
-		name := typ.Field(i).Tag.Get("flag")
-		if name == "" {
-			continue
-		}
-		kind := typ.Field(i).Type.Kind()
-		addr := val.Field(i).Addr()
-		switch kind {
-		case reflect.Bool:
-			pointer := (*bool)(unsafe.Pointer(addr.Pointer()))
-			flags.BoolVar(pointer, name, false, "")
-		case reflect.String:
-			pointer := (*string)(unsafe.Pointer(addr.Pointer()))
-			flags.StringVar(pointer, name, "", "")
-		case reflect.Int:
-			pointer := (*int)(unsafe.Pointer(addr.Pointer()))
-			flags.IntVar(pointer, name, -1, "")
-		case reflect.Int64:
-			pointer := (*int64)(unsafe.Pointer(addr.Pointer()))
-			flags.Int64Var(pointer, name, -1, "")
-		}
-	}
-
-	return flags.Parse(true, line[1:])
-}
-
-// Parse any non-flag argument, i.e. the subcommand, its parameters and any
-// extra argument following "--".
-func (p *Parser) parseRest(line []string, val reflect.Value) {
-	subcommand := ""
-	params := []string{}
-	extra := []string{}
-	if len(line) > 1 {
-		rest := line[1:]
-		for i, token := range rest {
-			if token == "--" {
-				// Set extra to anything left, excluding the token.
-				if i < len(rest)-1 {
-					extra = rest[i+1:]
-				}
-				break
-			}
-			if strings.HasPrefix(token, "-") {
-				// Subcommand and parameters must both come
-				// before any flag.
-				break
-			}
-			if i == 0 {
-				subcommand = token
-				continue
-			}
-			params = append(params, token)
-		}
-	}
-	if field := val.FieldByName("Subcommand"); field.IsValid() {
-		field.SetString(subcommand)
-	}
-	if field := val.FieldByName("Params"); field.IsValid() {
-		field.Set(reflect.ValueOf(params))
-	}
-	if field := val.FieldByName("Extra"); field.IsValid() {
-		field.Set(reflect.ValueOf(extra))
-	}
-}
diff --git a/shared/cmd/parser_test.go b/shared/cmd/parser_test.go
deleted file mode 100644
index 1b48a7eb8..000000000
--- a/shared/cmd/parser_test.go
+++ /dev/null
@@ -1,162 +0,0 @@
-package cmd_test
-
-import (
-	"strings"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-
-	"github.com/lxc/lxd/shared/cmd"
-	"github.com/lxc/lxd/shared/subtest"
-)
-
-// Sample command line arguments specification.
-type Args struct {
-	Subcommand string
-	Params     []string
-	Extra      []string
-
-	Help      bool   `flag:"help"`
-	Text      string `flag:"text"`
-	Number    int    `flag:"number"`
-	BigNumber int64  `flag:"big-number"`
-}
-
-// Check the default values of all command line args.
-func TestParser_ArgsDefaults(t *testing.T) {
-	line := []string{"cmd"}
-	args := &Args{}
-	parser := newParser()
-
-	assert.NoError(t, parser.Parse(line, args))
-
-	assert.Equal(t, "", args.Text)
-	assert.Equal(t, false, args.Help)
-	assert.Equal(t, -1, args.Number)
-	assert.Equal(t, int64(-1), args.BigNumber)
-}
-
-// Check that parsing the command line results in the correct attributes
-// being set.
-func TestParser_ArgsCustom(t *testing.T) {
-	line := []string{
-		"cmd",
-		"--text", "hello",
-		"--help",
-		"--number", "10",
-		"--big-number", "666",
-	}
-	args := &Args{}
-	parser := newParser()
-
-	assert.NoError(t, parser.Parse(line, args))
-
-	assert.Equal(t, "hello", args.Text)
-	assert.Equal(t, true, args.Help)
-	assert.Equal(t, 10, args.Number)
-	assert.Equal(t, int64(666), args.BigNumber)
-}
-
-// Check that the subcommand is properly set.
-func TestParser_Subcommand(t *testing.T) {
-	cases := []struct {
-		line       []string
-		subcommand string
-	}{
-		{[]string{"cmd"}, ""},
-		{[]string{"cmd", "--help"}, ""},
-		{[]string{"cmd", "subcmd"}, "subcmd"},
-		{[]string{"cmd", "subcmd", "--help"}, "subcmd"},
-		{[]string{"cmd", "--help", "subcmd"}, ""},
-	}
-	for _, c := range cases {
-		subtest.Run(t, strings.Join(c.line, "_"), func(t *testing.T) {
-			args := &Args{}
-			parser := newParser()
-			assert.NoError(t, parser.Parse(c.line, args))
-			assert.Equal(t, c.subcommand, args.Subcommand)
-		})
-	}
-}
-
-// Check that subcommand params are properly set.
-func TestParser_Params(t *testing.T) {
-	cases := []struct {
-		line   []string
-		params []string
-	}{
-		{[]string{"cmd"}, []string{}},
-		{[]string{"cmd", "--help"}, []string{}},
-		{[]string{"cmd", "subcmd"}, []string{}},
-		{[]string{"cmd", "subcmd", "param"}, []string{"param"}},
-		{[]string{"cmd", "subcmd", "param1", "param2"}, []string{"param1", "param2"}},
-		{[]string{"cmd", "subcmd", "param", "--help"}, []string{"param"}},
-		{[]string{"cmd", "subcmd", "--help", "param"}, []string{}},
-	}
-	for _, c := range cases {
-		subtest.Run(t, strings.Join(c.line, "_"), func(t *testing.T) {
-			args := &Args{}
-			parser := newParser()
-			assert.NoError(t, parser.Parse(c.line, args))
-			assert.Equal(t, c.params, args.Params)
-		})
-	}
-}
-
-// Check that extra params are properly set.
-func TestParser_Extra(t *testing.T) {
-	cases := []struct {
-		line  []string
-		extra []string
-	}{
-		{[]string{"cmd"}, []string{}},
-		{[]string{"cmd", "--help"}, []string{}},
-		{[]string{"cmd", "subcmd"}, []string{}},
-		{[]string{"cmd", "subcmd", "--"}, []string{}},
-		{[]string{"cmd", "subcmd", "--", "extra"}, []string{"extra"}},
-		{[]string{"cmd", "subcmd", "--", "extra1", "--extra2"}, []string{"extra1", "--extra2"}},
-	}
-	for _, c := range cases {
-		subtest.Run(t, strings.Join(c.line, "_"), func(t *testing.T) {
-			args := &Args{}
-			parser := newParser()
-			assert.NoError(t, parser.Parse(c.line, args))
-			assert.Equal(t, c.extra, args.Extra)
-		})
-	}
-}
-
-// If a flag doesn't exist, an error is returned.
-func TestParser_Error(t *testing.T) {
-	line := []string{"cmd", "--boom"}
-	args := &Args{}
-	parser := newParser()
-
-	assert.Error(t, parser.Parse(line, args))
-}
-
-// If a usage string is passed, and the command line has the help flag, the
-// message is printed out.
-func TestParser_Usage(t *testing.T) {
-	line := []string{"cmd", "-h"}
-	args := &Args{}
-	streams := cmd.NewMemoryStreams("")
-
-	parser := newParserWithStreams(streams)
-	parser.UsageMessage = "usage message"
-
-	assert.Error(t, parser.Parse(line, args))
-	assert.Equal(t, parser.UsageMessage, streams.Out())
-}
-
-// Return a new test parser
-func newParser() *cmd.Parser {
-	return newParserWithStreams(cmd.NewMemoryStreams(""))
-}
-
-// Return a new test parser using the given streams for its context.
-func newParserWithStreams(streams *cmd.MemoryStreams) *cmd.Parser {
-	return &cmd.Parser{
-		Context: cmd.NewMemoryContext(streams),
-	}
-}
diff --git a/shared/cmd/testing.go b/shared/cmd/testing.go
deleted file mode 100644
index bb5eca24f..000000000
--- a/shared/cmd/testing.go
+++ /dev/null
@@ -1,77 +0,0 @@
-// In-memory streams, useful for testing cmd-related code.
-
-package cmd
-
-import (
-	"bytes"
-	"io/ioutil"
-	"strings"
-)
-
-// MemoryStreams provide an in-memory version of the system
-// stdin/stdout/stderr streams.
-type MemoryStreams struct {
-	in  *strings.Reader
-	out *bytes.Buffer
-	err *bytes.Buffer
-}
-
-// NewMemoryStreams creates a new set of in-memory streams with the given
-// user input.
-func NewMemoryStreams(input string) *MemoryStreams {
-	return &MemoryStreams{
-		in:  strings.NewReader(input),
-		out: new(bytes.Buffer),
-		err: new(bytes.Buffer),
-	}
-}
-
-// InputRead returns the current input string.
-func (s *MemoryStreams) InputRead() string {
-	bytes, _ := ioutil.ReadAll(s.in)
-	return string(bytes)
-}
-
-// Out returns the current content of the out stream.
-func (s *MemoryStreams) Out() string {
-	return s.out.String()
-}
-
-// Err returns the current content of the err stream.
-func (s *MemoryStreams) Err() string {
-	return s.err.String()
-}
-
-// InputReset replaces the data in the input stream.
-func (s *MemoryStreams) InputReset(input string) {
-	// XXX This is what the stdlib strings.Reader.Reset() does, however
-	//     this method is not available in Go 1.6.
-	*s.in = *strings.NewReader(input)
-}
-
-// InputAppend adds the given text to the current input.
-func (s *MemoryStreams) InputAppend(text string) {
-	s.InputReset(s.InputRead() + text)
-}
-
-// InputAppendLine adds a single line to the input stream.
-func (s *MemoryStreams) InputAppendLine(line string) {
-	s.InputAppend(line + "\n")
-}
-
-// InputAppendBoolAnswer adds a new "yes" or "no" line depending on the answer.
-func (s *MemoryStreams) InputAppendBoolAnswer(answer bool) {
-	var line string
-	if answer {
-		line = "yes"
-	} else {
-		line = "no"
-	}
-	s.InputAppendLine(line)
-}
-
-// NewMemoryContext creates a new command Context using the given in-memory
-// streams.
-func NewMemoryContext(streams *MemoryStreams) *Context {
-	return NewContext(streams.in, streams.out, streams.err)
-}
diff --git a/test/includes/clustering.sh b/test/includes/clustering.sh
index 0b381783d..dc800b6e1 100644
--- a/test/includes/clustering.sh
+++ b/test/includes/clustering.sh
@@ -273,7 +273,7 @@ cluster:
   enabled: true
   cluster_address: 10.1.1.10${target}:8443
   cluster_certificate: "$cert"
-cluster_password: sekret
+  cluster_password: sekret
 EOF
   lxd init --preseed < "${LXD_DIR}/preseed.yaml"
   )
diff --git a/test/suites/init_interactive.sh b/test/suites/init_interactive.sh
index ab7ec40db..2ebc0fcc7 100644
--- a/test/suites/init_interactive.sh
+++ b/test/suites/init_interactive.sh
@@ -20,12 +20,13 @@ no
 yes
 my-storage-pool
 dir
-no
-no
 yes
 lxdt$$
 auto
 none
+no
+no
+yes
 EOF
 
     lxc info | grep -q 'images.auto_update_interval: "0"'

From 6ae8107d1f0b39f55f7ec20701c97bf5d96ac852 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 21 Mar 2018 01:05:18 -0400
Subject: [PATCH 19/19] lxd: Cleanup leftovers after port
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/main_args.go | 141 -------------------------------------------------------
 1 file changed, 141 deletions(-)
 delete mode 100644 lxd/main_args.go

diff --git a/lxd/main_args.go b/lxd/main_args.go
deleted file mode 100644
index 8f6b0848e..000000000
--- a/lxd/main_args.go
+++ /dev/null
@@ -1,141 +0,0 @@
-package main
-
-// Args contains all supported LXD command line flags.
-type Args struct {
-	Auto                 bool   `flag:"auto"`
-	Preseed              bool   `flag:"preseed"`
-	CPUProfile           string `flag:"cpuprofile"`
-	Debug                bool   `flag:"debug"`
-	Trace                string `flag:"trace"`
-	Group                string `flag:"group"`
-	Help                 bool   `flag:"help"`
-	Logfile              string `flag:"logfile"`
-	MemProfile           string `flag:"memprofile"`
-	NetworkAddress       string `flag:"network-address"`
-	NetworkPort          int64  `flag:"network-port"`
-	PrintGoroutinesEvery int    `flag:"print-goroutines-every"`
-	StorageBackend       string `flag:"storage-backend"`
-	StorageCreateDevice  string `flag:"storage-create-device"`
-	StorageCreateLoop    int64  `flag:"storage-create-loop"`
-	StorageDataset       string `flag:"storage-pool"`
-	Syslog               bool   `flag:"syslog"`
-	Timeout              int    `flag:"timeout"`
-	TrustPassword        string `flag:"trust-password"`
-	Verbose              bool   `flag:"verbose"`
-	Version              bool   `flag:"version"`
-	Force                bool   `flag:"force"`
-
-	// The LXD subcommand, if any (e.g. "init" for "lxd init")
-	Subcommand string
-
-	// The subcommand parameters (e.g. []string{"foo"} for "lxd import foo").
-	Params []string
-
-	// Any extra arguments following the "--" separator.
-	Extra []string
-}
-
-const usage = `Usage: lxd [command] [options]
-
-Commands:
-    activateifneeded
-        Check if LXD should be started (at boot) and if so, spawns it through socket activation
-    daemon [--group=lxd] (default command)
-        Start the main LXD daemon
-    init [--auto] [--network-address=IP] [--network-port=8443] [--storage-backend=dir]
-         [--storage-create-device=DEVICE] [--storage-create-loop=SIZE] [--storage-pool=POOL]
-         [--trust-password=] [--preseed]
-        Setup storage and networking
-    ready
-        Tells LXD that any setup-mode configuration has been done and that it can start containers.
-    shutdown [--timeout=60]
-        Perform a clean shutdown of LXD and all running containers
-    waitready [--timeout=15]
-        Wait until LXD is ready to handle requests
-    import <container name> [--force]
-        Import a pre-existing container from storage
-
-
-Common options:
-    --debug
-        Enable debug mode
-    --trace SUBSYSTEMS
-        Enable trace logging for the given comma-separated list of sub-systems (e.g. dqlite,raft)
-    --help
-        Print this help message
-    --logfile FILE
-        Logfile to log to (e.g., /var/log/lxd/lxd.log)
-    --syslog
-        Enable syslog logging
-    --verbose
-        Enable verbose mode
-    --version
-        Print LXD's version number and exit
-
-Daemon options:
-    --group GROUP
-        Group which owns the shared socket (ignored with socket-based activation)
-
-Daemon debug options:
-    --cpuprofile FILE
-        Enable cpu profiling into the specified file
-    --memprofile FILE
-        Enable memory profiling into the specified file
-    --print-goroutines-every SECONDS
-        For debugging, print a complete stack trace every n seconds
-
-Init options:
-    --auto
-        Automatic (non-interactive) mode
-    --preseed
-        Pre-seed mode, expects YAML config from stdin
-
-Init options for non-interactive mode (--auto):
-    --network-address ADDRESS
-        Address to bind LXD to (default: none)
-    --network-port PORT
-        Port to bind LXD to (default: 8443)
-    --storage-backend NAME
-        Storage backend to use (btrfs, dir, lvm or zfs, default: dir)
-    --storage-create-device DEVICE
-        Setup device based storage using DEVICE
-    --storage-create-loop SIZE
-        Setup loop based storage with SIZE in GB
-    --storage-pool NAME
-        Storage pool to use or create
-    --trust-password PASSWORD
-        Password required to add new clients
-
-Shutdown options:
-    --timeout SECONDS
-        How long to wait before failing
-
-Waitready options:
-    --timeout SECONDS
-        How long to wait before failing
-
-
-Internal commands (don't call these directly):
-    forkconsole
-        Attach to the console of a container
-    forkexec
-        Execute a command in a container
-    forkgetnet
-        Get container network information
-    forkgetfile
-        Grab a file from a running container
-    forkmigrate
-        Restore a container after migration
-    forkputfile
-        Push a file to a running container
-    forkstart
-        Start a container
-    callhook
-        Call a container hook
-    migratedumpsuccess
-        Indicate that a migration dump was successful
-    netcat
-        Mirror a unix socket to stdin/stdout
-    forkproxy
-        Start proxy device process for a container
-`


More information about the lxc-devel mailing list