[lxc-devel] [lxd/master] Implement initial support for "lxd init --preseed"

freeekanayaka on Github lxc-bot at linuxcontainers.org
Wed May 3 06:50:11 UTC 2017


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 1890 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20170503/fdc7d76c/attachment.bin>
-------------- next part --------------
From 33958cc070d39a75a269ab7b13ec1350213d401d Mon Sep 17 00:00:00 2001
From: Free Ekanayaka <free.ekanayaka at canonical.com>
Date: Wed, 3 May 2017 08:33:13 +0200
Subject: [PATCH] Implement initial support for "lxd init --preseed"

Add all the initial code and testing plumbing for the init pre-seed
feature.

In particular:

- A new --preseed option has been added and briefly documented under
  the section for the "init" command. We'll want to add more docs down
  the road.

- A minimal implementation of --preseed as been added, for now
  supporting only network address and trust password.

- The existing auto/interactive implementation of the init command has
  been changed to share the code that preseed uses (which will be more
  general, since preseed is going to be more featureful than
  auto/interactive). This type of change will continue gradually in
  follow-up branches.

- Unit tests have been added for the preseed case, and also for auto
  and interactive.

- A new minimal test/suites/init_preseed.sh integration test has been
  added. It will grow in follow-up branches.

- The existing test/suite/init.sh test has been renamed to
  init_auto.sh, to keep files short.

As per the format of the input YAML, I went for simply:

```
config:
  core.https_address: 127.0.0.1:9999
```

vs.:

```
daemon:
  config:
    core.https_address: 127.0.0.1:9999
```

since it seems simpler and un-ambiguous at the same time, and it's
probably more consistent with the REST API (you'd post/put the daemon
config to the root /1.0 resource).

Follow-up branches will add more sections and specified in the ticket:

```
config:
  core.https_address: 127.0.0.1:9999
pools:
  ...
networks:
  ...
```

Signed-off-by: Free Ekanayaka <free.ekanayaka at canonical.com>
---
 lxd/main.go                           |   5 +-
 lxd/main_init.go                      | 107 +++++++++++++++++++++++++++++-----
 lxd/main_init_test.go                 |  97 ++++++++++++++++++++++++++++--
 lxd/main_test.go                      |  15 ++++-
 shared/cmd/context.go                 |  17 ++++++
 shared/cmd/context_test.go            |  22 +++++++
 shared/cmd/testing.go                 |  33 +++++++++++
 test/main.sh                          |   3 +-
 test/suites/{init.sh => init_auto.sh} |   2 +-
 test/suites/init_preseed.sh           |  15 +++++
 10 files changed, 291 insertions(+), 25 deletions(-)
 rename test/suites/{init.sh => init_auto.sh} (99%)
 create mode 100644 test/suites/init_preseed.sh

diff --git a/lxd/main.go b/lxd/main.go
index 6901813..fe965a9 100644
--- a/lxd/main.go
+++ b/lxd/main.go
@@ -15,6 +15,7 @@ import (
 
 // Global arguments
 var argAuto = gnuflag.Bool("auto", false, "")
+var argPreseed = gnuflag.Bool("preseed", false, "")
 var argCPUProfile = gnuflag.String("cpuprofile", "", "")
 var argDebug = gnuflag.Bool("debug", false, "")
 var argGroup = gnuflag.String("group", "", "")
@@ -68,7 +69,7 @@ func run() error {
 		fmt.Printf("        Start the main LXD daemon\n")
 		fmt.Printf("    init [--auto] [--network-address=IP] [--network-port=8443] [--storage-backend=dir]\n")
 		fmt.Printf("         [--storage-create-device=DEVICE] [--storage-create-loop=SIZE] [--storage-pool=POOL]\n")
-		fmt.Printf("         [--trust-password=]\n")
+		fmt.Printf("         [--trust-password=] [--preseed]\n")
 		fmt.Printf("        Setup storage and networking\n")
 		fmt.Printf("    ready\n")
 		fmt.Printf("        Tells LXD that any setup-mode configuration has been done and that it can start containers.\n")
@@ -108,6 +109,8 @@ func run() error {
 		fmt.Printf("\nInit options:\n")
 		fmt.Printf("    --auto\n")
 		fmt.Printf("        Automatic (non-interactive) mode\n")
+		fmt.Printf("    --preseed\n")
+		fmt.Printf("        Pre-seed mode, expects YAML config from stdin\n")
 
 		fmt.Printf("\nInit options for non-interactive mode (--auto):\n")
 		fmt.Printf("    --network-address ADDRESS\n")
diff --git a/lxd/main_init.go b/lxd/main_init.go
index 89f25ab..ac9f0c2 100644
--- a/lxd/main_init.go
+++ b/lxd/main_init.go
@@ -20,6 +20,7 @@ import (
 // CmdInitArgs holds command line arguments for the "lxd init" command.
 type CmdInitArgs struct {
 	Auto                bool
+	Preseed             bool
 	StorageBackend      string
 	StorageCreateDevice string
 	StorageCreateLoop   int64
@@ -40,6 +41,40 @@ type CmdInit struct {
 
 // 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
+	}
+
+	// Connect to LXD
+	client, err := lxd.ConnectLXDUnix(cmd.SocketPath, nil)
+	if err != nil {
+		return fmt.Errorf("Unable to talk to LXD: %s", err)
+	}
+
+	// Kick off the appropriate run mode (either preseed, auto or interactive).
+	if cmd.Args.Preseed {
+		err = cmd.runPreseed(client)
+	} else {
+		err = cmd.runAutoOrInteractive(client)
+	}
+
+	if err == nil {
+		cmd.Context.Output("LXD has been successfully configured.\n")
+	}
+
+	return err
+}
+
+// Run the logic for auto or interactive mode.
+//
+// IMPLEMENTATION NOTE FOR REVIEWERS (free): this logic is going to be
+// refactored into two separate runAuto and runInteractive methods,
+// sharing relevant logic with runPreseed. The idea being that both
+// runAuto and runInteractive will end up populating the same
+// low-level cmdInitData structure passed to the common run() method.
+func (cmd *CmdInit) runAutoOrInteractive(c lxd.ContainerServer) error {
 	var defaultPrivileged int // controls whether we set security.privileged=true
 	var storageSetup bool     // == supportedStoragePoolDrivers
 	var storageBackend string // == supportedStoragePoolDrivers
@@ -85,12 +120,6 @@ func (cmd *CmdInit) Run() error {
 		backendsAvailable = append(backendsAvailable, driver)
 	}
 
-	// Connect to LXD
-	c, err := lxd.ConnectLXDUnix(cmd.SocketPath, nil)
-	if err != nil {
-		return fmt.Errorf("Unable to talk to LXD: %s", err)
-	}
-
 	pools, err := c.GetStoragePoolNames()
 	if err != nil {
 		// We should consider this fatal since this means
@@ -447,20 +476,20 @@ they otherwise would.
 		}
 	}
 
-	if networkAddress != "" {
-		err = cmd.setServerConfig(c, "core.https_address", fmt.Sprintf("%s:%d", networkAddress, networkPort))
-		if err != nil {
-			return err
-		}
+	data := &cmdInitData{}
+	data.Config = map[string]interface{}{}
 
+	if networkAddress != "" {
+		data.Config["core.https_address"] = fmt.Sprintf("%s:%d", networkAddress, networkPort)
 		if trustPassword != "" {
-			err = cmd.setServerConfig(c, "core.trust_password", trustPassword)
-			if err != nil {
-				return err
-			}
+			data.Config["core.trust_password"] = trustPassword
 		}
 	}
 
+	if err := cmd.run(c, data); err != nil {
+		return err
+	}
+
 	if bridgeName != "" {
 		bridgeConfig := map[string]string{}
 		bridgeConfig["ipv4.address"] = bridgeIPv4
@@ -494,8 +523,46 @@ they otherwise would.
 			return err
 		}
 	}
+	return nil
+}
+
+// Run the logic for preseed mode
+func (cmd *CmdInit) runPreseed(client lxd.ContainerServer) error {
+	data := &cmdInitData{}
+
+	if err := cmd.Context.InputYAML(data); err != nil {
+		return fmt.Errorf("Invalid preseed YAML content")
+	}
 
-	fmt.Printf("LXD has been successfully configured.\n")
+	return cmd.run(client, data)
+}
+
+// Apply the configuration specified in the given init data.
+func (cmd *CmdInit) run(client lxd.ContainerServer, data *cmdInitData) error {
+	if err := cmd.initConfig(client, data.Config); err != nil {
+		return err
+	}
+	return nil
+}
+
+// Apply the server-level configuration in the given map.
+func (cmd *CmdInit) initConfig(client lxd.ContainerServer, config map[string]interface{}) error {
+	server, etag, err := client.GetServer()
+	if err != nil {
+		return err
+	}
+
+	server.Config = config
+
+	return client.UpdateServer(server.Writable(), etag)
+}
+
+// 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")
+	}
 	return nil
 }
 
@@ -564,10 +631,18 @@ func (cmd *CmdInit) setProfileConfigItem(c lxd.ContainerServer, profileName stri
 	return nil
 }
 
+// Defines the schema for all possible configuration knobs supported by the
+// lxd init command, either directly feeded via --preseed or populated by
+// the auto/interactive modes.
+type cmdInitData struct {
+	api.ServerPut `yaml:",inline"`
+}
+
 func cmdInit() error {
 	context := cmd.NewContext(os.Stdin, os.Stdout, os.Stderr)
 	args := &CmdInitArgs{
 		Auto:                *argAuto,
+		Preseed:             *argPreseed,
 		StorageBackend:      *argStorageBackend,
 		StorageCreateDevice: *argStorageCreateDevice,
 		StorageCreateLoop:   *argStorageCreateLoop,
diff --git a/lxd/main_init_test.go b/lxd/main_init_test.go
index 0c4b0bd..f0773be 100644
--- a/lxd/main_init_test.go
+++ b/lxd/main_init_test.go
@@ -9,14 +9,16 @@ import (
 
 type cmdInitTestSuite struct {
 	lxdTestSuite
+	streams *cmd.MemoryStreams
 	context *cmd.Context
 	args    *CmdInitArgs
 	command *CmdInit
 }
 
-func (suite *cmdInitTestSuite) SetupSuite() {
-	suite.lxdTestSuite.SetupSuite()
-	suite.context = cmd.NewMemoryContext(cmd.NewMemoryStreams(""))
+func (suite *cmdInitTestSuite) SetupTest() {
+	suite.lxdTestSuite.SetupTest()
+	suite.streams = cmd.NewMemoryStreams("")
+	suite.context = cmd.NewMemoryContext(suite.streams)
 	suite.args = &CmdInitArgs{
 		NetworkPort:       -1,
 		StorageCreateLoop: -1,
@@ -34,7 +36,94 @@ func (suite *cmdInitTestSuite) SetupSuite() {
 func (suite *cmdInitTestSuite) TestCmdInit_InteractiveWithAutoArgs() {
 	suite.args.NetworkPort = 9999
 	err := suite.command.Run()
-	suite.Req.Equal(err.Error(), "Init configuration is only valid with --auto")
+	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())
+}
+
+// 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() {
+	suite.args.Preseed = true
+	suite.streams.InputAppend(`config:
+  core.https_address: 127.0.0.1:9999
+  core.trust_password: sekret
+`)
+	suite.Req.Nil(suite.command.Run())
+
+	key, _ := daemonConfig["core.https_address"]
+	suite.Req.Equal("127.0.0.1:9999", key.Get())
+	suite.Req.Nil(suite.d.PasswordCheck("sekret"))
+}
+
+// Input network address and trust password interactively.
+func (suite *cmdInitTestSuite) TestCmdInit_InteractiveHTTPSAddressAndTrustPassword() {
+	suite.command.PasswordReader = func(int) ([]byte, error) {
+		return []byte("sekret"), nil
+	}
+	answers := &cmdInitAnswers{
+		WantAvailableOverNetwork: true,
+		BindToAddress:            "127.0.0.1",
+		BindToPort:               "9999",
+	}
+	answers.Render(suite.streams)
+
+	suite.Req.Nil(suite.command.Run())
+
+	key, _ := daemonConfig["core.https_address"]
+	suite.Req.Equal("127.0.0.1:9999", key.Get())
+	suite.Req.Nil(suite.d.PasswordCheck("sekret"))
+}
+
+// Pass network address and trust password via command line arguments.
+func (suite *cmdInitTestSuite) TestCmdInit_AutoHTTPSAddressAndTrustPassword() {
+	suite.args.Auto = true
+	suite.args.NetworkAddress = "127.0.0.1"
+	suite.args.NetworkPort = 9999
+	suite.args.TrustPassword = "sekret"
+
+	suite.Req.Nil(suite.command.Run())
+
+	key, _ := daemonConfig["core.https_address"]
+	suite.Req.Equal("127.0.0.1:9999", key.Get())
+	suite.Req.Nil(suite.d.PasswordCheck("sekret"))
+}
+
+// Convenience for building the input text a user would enter for a certain
+// sequence of answers.
+type cmdInitAnswers struct {
+	WantStoragePool          bool
+	WantAvailableOverNetwork bool
+	BindToAddress            string
+	BindToPort               string
+	WantImageAutoUpdate      bool
+	WantNetworkBridge        bool
+}
+
+// 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.WantStoragePool)
+	streams.InputAppendBoolAnswer(answers.WantAvailableOverNetwork)
+	if answers.WantAvailableOverNetwork {
+		streams.InputAppendLine(answers.BindToAddress)
+		streams.InputAppendLine(answers.BindToPort)
+	}
+	streams.InputAppendBoolAnswer(answers.WantImageAutoUpdate)
+	streams.InputAppendBoolAnswer(answers.WantNetworkBridge)
 }
 
 func TestCmdInitTestSuite(t *testing.T) {
diff --git a/lxd/main_test.go b/lxd/main_test.go
index a6828c8..271371a 100644
--- a/lxd/main_test.go
+++ b/lxd/main_test.go
@@ -13,9 +13,20 @@ import (
 )
 
 func mockStartDaemon() (*Daemon, error) {
+	certBytes, keyBytes, err := shared.GenerateMemCert(false)
+	if err != nil {
+		return nil, err
+	}
+	cert, err := tls.X509KeyPair(certBytes, keyBytes)
+	if err != nil {
+		return nil, err
+	}
+
 	d := &Daemon{
-		MockMode:  true,
-		tlsConfig: &tls.Config{},
+		MockMode: true,
+		tlsConfig: &tls.Config{
+			Certificates: []tls.Certificate{cert},
+		},
 	}
 
 	if err := d.Init(); err != nil {
diff --git a/shared/cmd/context.go b/shared/cmd/context.go
index 346657c..ef192ce 100644
--- a/shared/cmd/context.go
+++ b/shared/cmd/context.go
@@ -3,7 +3,9 @@ package cmd
 import (
 	"bufio"
 	"fmt"
+	"gopkg.in/yaml.v2"
 	"io"
+	"io/ioutil"
 	"strconv"
 	"strings"
 
@@ -27,6 +29,11 @@ func NewContext(stdin io.Reader, stdout, stderr io.Writer) *Context {
 	}
 }
 
+// Output prints a message on standard output.
+func (c *Context) Output(format string, a ...interface{}) {
+	fmt.Fprintf(c.stdout, format, a...)
+}
+
 // AskBool asks a question an expect a yes/no answer.
 func (c *Context) AskBool(question string, defaultAnswer string) bool {
 	for {
@@ -117,6 +124,16 @@ func (c *Context) AskPassword(question string, reader func(int) ([]byte, error))
 	}
 }
 
+// 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)
diff --git a/shared/cmd/context_test.go b/shared/cmd/context_test.go
index 24006b0..443541e 100644
--- a/shared/cmd/context_test.go
+++ b/shared/cmd/context_test.go
@@ -8,6 +8,14 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+// 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")
+	streams.AssertOutEqual(t, "Hello world")
+}
+
 // AskBool returns a boolean result depending on the user input.
 func TestAskBool(t *testing.T) {
 	cases := []struct {
@@ -146,3 +154,17 @@ func TestAskPassword(t *testing.T) {
 		streams.AssertErrEqual(t, c.error)
 	}
 }
+
+// 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/testing.go b/shared/cmd/testing.go
index faefb48..745f87e 100644
--- a/shared/cmd/testing.go
+++ b/shared/cmd/testing.go
@@ -4,6 +4,7 @@ package cmd
 
 import (
 	"bytes"
+	"io/ioutil"
 	"strings"
 	"testing"
 
@@ -28,6 +29,38 @@ func NewMemoryStreams(input string) *MemoryStreams {
 	}
 }
 
+// InputRead returns the current input string.
+func (s *MemoryStreams) InputRead() string {
+	bytes, _ := ioutil.ReadAll(s.in)
+	return string(bytes)
+}
+
+// InputReset replaces the data in the input stream.
+func (s *MemoryStreams) InputReset(input string) {
+	s.in.Reset(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)
+}
+
 // AssertOutEqual checks that the given text matches the the out stream.
 func (s *MemoryStreams) AssertOutEqual(t *testing.T, expected string) {
 	assert.Equal(t, expected, s.out.String(), "Unexpected output stream")
diff --git a/test/main.sh b/test/main.sh
index d470070..5177a9a 100755
--- a/test/main.sh
+++ b/test/main.sh
@@ -624,7 +624,8 @@ run_test test_fdleak "fd leak"
 run_test test_cpu_profiling "CPU profiling"
 run_test test_mem_profiling "memory profiling"
 run_test test_storage "storage"
-run_test test_lxd_autoinit "lxd init auto"
+run_test test_init_auto "lxd init auto"
+run_test test_init_preseed "lxd init auto"
 run_test test_storage_profiles "storage profiles"
 run_test test_container_import "container import"
 
diff --git a/test/suites/init.sh b/test/suites/init_auto.sh
similarity index 99%
rename from test/suites/init.sh
rename to test/suites/init_auto.sh
index 8f5790d..2617dcc 100644
--- a/test/suites/init.sh
+++ b/test/suites/init_auto.sh
@@ -1,4 +1,4 @@
-test_lxd_autoinit() {
+test_init_auto() {
   # - lxd init --auto --storage-backend zfs
   # and
   # - lxd init --auto
diff --git a/test/suites/init_preseed.sh b/test/suites/init_preseed.sh
new file mode 100644
index 0000000..24db5e1
--- /dev/null
+++ b/test/suites/init_preseed.sh
@@ -0,0 +1,15 @@
+test_init_preseed() {
+  # - lxd init --preseed
+  LXD_INIT_DIR=$(mktemp -d -p "${TEST_DIR}" XXX)
+  chmod +x "${LXD_INIT_DIR}"
+  spawn_lxd "${LXD_INIT_DIR}" false
+
+  cat <<EOF | LXD_DIR=${LXD_INIT_DIR} lxd init --preseed
+config:
+  core.https_address: 127.0.0.1:9999
+EOF
+
+  kill_lxd "${LXD_INIT_DIR}"
+
+  return
+}


More information about the lxc-devel mailing list