[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