[lxc-devel] [lxd/master] Add MAAS integration

stgraber on Github lxc-bot at linuxcontainers.org
Tue Dec 19 04:28:11 UTC 2017


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/20171219/e6aa3d38/attachment.bin>
-------------- next part --------------
From 42e329a09c67f9d8a10b621ba13d79993c1de608 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 13 Dec 2017 03:17:25 -0500
Subject: [PATCH 1/4] lxd/maas: Add MAAS integration package
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/maas/controller.go | 377 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 377 insertions(+)
 create mode 100644 lxd/maas/controller.go

diff --git a/lxd/maas/controller.go b/lxd/maas/controller.go
new file mode 100644
index 000000000..59eb3dc3d
--- /dev/null
+++ b/lxd/maas/controller.go
@@ -0,0 +1,377 @@
+package maas
+
+import (
+	"fmt"
+	"net/url"
+
+	"github.com/juju/gomaasapi"
+)
+
+// Controller represents a MAAS server's machine functions
+type Controller struct {
+	url string
+
+	srv     gomaasapi.Controller
+	srvRaw  gomaasapi.Client
+	machine gomaasapi.Machine
+}
+
+// ContainerInterface represents a MAAS connected network interface on the container
+type ContainerInterface struct {
+	Name       string
+	MACAddress string
+	Subnets    []ContainerInterfaceSubnet
+}
+
+// ContainerInterfaceSubnet represents an interface's subscription to a MAAS subnet
+type ContainerInterfaceSubnet struct {
+	Name    string
+	Address string
+}
+
+func parseInterfaces(interfaces []ContainerInterface) (map[string]ContainerInterface, error) {
+	// Sanity checks
+	if len(interfaces) == 0 {
+		return nil, fmt.Errorf("At least one interface must be provided")
+	}
+
+	// Parse the MACs and interfaces
+	macInterfaces := map[string]ContainerInterface{}
+	for _, iface := range interfaces {
+		_, ok := macInterfaces[iface.MACAddress]
+		if ok {
+			return nil, fmt.Errorf("MAAS doesn't allow duplicate MAC addresses")
+		}
+
+		if iface.MACAddress == "" {
+			return nil, fmt.Errorf("Interfaces must have a MAC address")
+		}
+
+		if len(iface.Subnets) == 0 {
+			return nil, fmt.Errorf("Interfaces must have at least one subnet")
+		}
+
+		macInterfaces[iface.MACAddress] = iface
+	}
+
+	return macInterfaces, nil
+}
+
+// NewController returns a new Controller using the specific MAAS server and machine
+func NewController(url string, key string, machine string) (*Controller, error) {
+	baseURL := fmt.Sprintf("%s/api/2.0/", url)
+
+	// Connect to MAAS
+	srv, err := gomaasapi.NewController(gomaasapi.ControllerArgs{
+		BaseURL: baseURL,
+		APIKey:  key,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	srvRaw, err := gomaasapi.NewAuthenticatedClient(baseURL, key)
+	if err != nil {
+		return nil, err
+	}
+
+	// Find the right machine
+	machines, err := srv.Machines(gomaasapi.MachinesArgs{Hostnames: []string{machine}})
+	if err != nil {
+		return nil, err
+	}
+
+	if len(machines) != 1 {
+		return nil, fmt.Errorf("Couldn't find the specified machine: %s", machine)
+	}
+
+	// Setup the struct
+	c := Controller{}
+	c.srv = srv
+	c.srvRaw = *srvRaw
+	c.machine = machines[0]
+	c.url = baseURL
+
+	return &c, err
+}
+
+func (c *Controller) getDevice(name string) (gomaasapi.Device, error) {
+	devs, err := c.machine.Devices(gomaasapi.DevicesArgs{Hostname: []string{name}})
+	if err != nil {
+		return nil, err
+	}
+
+	if len(devs) != 1 {
+		return nil, fmt.Errorf("Couldn't find the specified container: %s", name)
+	}
+
+	return devs[0], nil
+}
+
+func (c *Controller) getSubnets() (map[string]gomaasapi.Subnet, error) {
+	// Get all the spaces
+	spaces, err := c.srv.Spaces()
+	if err != nil {
+		return nil, err
+	}
+
+	// Get all the subnets
+	subnets := map[string]gomaasapi.Subnet{}
+	for _, space := range spaces {
+		for _, subnet := range space.Subnets() {
+			subnets[subnet.Name()] = subnet
+		}
+	}
+
+	return subnets, nil
+}
+
+// CreateContainer defines a new MAAS device for the controller
+func (c *Controller) CreateContainer(name string, interfaces []ContainerInterface) error {
+	// Parse the provided interfaces
+	macInterfaces, err := parseInterfaces(interfaces)
+	if err != nil {
+		return err
+	}
+
+	// Get all the subnets
+	subnets, err := c.getSubnets()
+	if err != nil {
+		return err
+	}
+
+	// Create the device and first interface
+	device, err := c.machine.CreateDevice(gomaasapi.CreateMachineDeviceArgs{
+		Hostname:      name,
+		InterfaceName: interfaces[0].Name,
+		MACAddress:    interfaces[0].MACAddress,
+		VLAN:          subnets[interfaces[0].Subnets[0].Name].VLAN(),
+	})
+	if err != nil {
+		return err
+	}
+
+	// Wipe the container entry if anything fails
+	success := false
+	defer func() {
+		if success == true {
+			return
+		}
+
+		c.DeleteContainer(name)
+	}()
+
+	// Create the rest of the interfaces
+	for _, iface := range interfaces[1:] {
+		_, err := device.CreateInterface(gomaasapi.CreateInterfaceArgs{
+			Name:       iface.Name,
+			MACAddress: iface.MACAddress,
+			VLAN:       subnets[iface.Subnets[0].Name].VLAN(),
+		})
+		if err != nil {
+			return err
+		}
+	}
+
+	// Get a fresh copy of the device
+	device, err = c.getDevice(name)
+	if err != nil {
+		return err
+	}
+
+	// Setup the interfaces
+	for _, entry := range device.InterfaceSet() {
+		// Get our record
+		iface, ok := macInterfaces[entry.MACAddress()]
+		if !ok {
+			return fmt.Errorf("MAAS created an interface with a bad MAC: %s", entry.MACAddress())
+		}
+
+		// Add the subnets
+		for _, subnet := range iface.Subnets {
+			err := entry.LinkSubnet(gomaasapi.LinkSubnetArgs{
+				Mode:      gomaasapi.LinkModeStatic,
+				Subnet:    subnets[subnet.Name],
+				IPAddress: subnet.Address,
+			})
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	success = true
+	return nil
+}
+
+// DefinedContainer returns true if the container is defined in MAAS
+func (c *Controller) DefinedContainer(name string) (bool, error) {
+	devs, err := c.machine.Devices(gomaasapi.DevicesArgs{Hostname: []string{name}})
+	if err != nil {
+		return false, err
+	}
+
+	if len(devs) == 1 {
+		return true, nil
+	}
+
+	return false, nil
+}
+
+// UpdateContainer updates the MAAS device's interfaces with the new provided state
+func (c *Controller) UpdateContainer(name string, interfaces []ContainerInterface) error {
+	// Parse the provided interfaces
+	macInterfaces, err := parseInterfaces(interfaces)
+	if err != nil {
+		return err
+	}
+
+	// Get all the subnets
+	subnets, err := c.getSubnets()
+	if err != nil {
+		return err
+	}
+
+	device, err := c.getDevice(name)
+	if err != nil {
+		return err
+	}
+
+	// Iterate over existing interfaces, drop all removed ones and update existing ones
+	existingInterfaces := map[string]gomaasapi.Interface{}
+	for _, entry := range device.InterfaceSet() {
+		// Check if the interface has been removed from the container
+		iface, ok := macInterfaces[entry.MACAddress()]
+		if !ok {
+			// Delete the interface in MAAS
+			err = entry.Delete()
+			if err != nil {
+				return err
+			}
+
+			continue
+		}
+
+		// Update the subnets
+		existingSubnets := map[string]gomaasapi.Subnet{}
+		for _, link := range entry.Links() {
+			// Check if the MAAS subnet matches any of the container's
+			found := false
+			for _, subnet := range iface.Subnets {
+				if subnet.Name == link.Subnet().Name() {
+					if subnet.Address == "" || subnet.Address == link.IPAddress() {
+						found = true
+					}
+					break
+				}
+			}
+
+			// If no exact match could be found, remove it from MAAS
+			if !found {
+				err = entry.UnlinkSubnet(link.Subnet())
+				if err != nil {
+					return err
+				}
+
+				continue
+			}
+
+			// Record the existing up to date subnet
+			existingSubnets[link.Subnet().Name()] = link.Subnet()
+		}
+
+		// Add any missing (or updated) subnet to MAAS
+		for _, subnet := range iface.Subnets {
+			// Check that it's not configured yet
+			_, ok := existingSubnets[subnet.Name]
+			if ok {
+				continue
+			}
+
+			// Add the link
+			err := entry.LinkSubnet(gomaasapi.LinkSubnetArgs{
+				Mode:      gomaasapi.LinkModeStatic,
+				Subnet:    subnets[subnet.Name],
+				IPAddress: subnet.Address,
+			})
+			if err != nil {
+				return err
+			}
+		}
+
+		// Record the interface has being configured
+		existingInterfaces[entry.MACAddress()] = entry
+	}
+
+	// Iterate over expected interfaces, add any missing one
+	for _, iface := range macInterfaces {
+		_, ok := existingInterfaces[iface.MACAddress]
+		if ok {
+			// We already have it so just move on
+			continue
+		}
+
+		// Create the new interface
+		entry, err := device.CreateInterface(gomaasapi.CreateInterfaceArgs{
+			Name:       iface.Name,
+			MACAddress: iface.MACAddress,
+			VLAN:       subnets[iface.Subnets[0].Name].VLAN(),
+		})
+		if err != nil {
+			return err
+		}
+
+		// Add the subnets
+		for _, subnet := range iface.Subnets {
+			err := entry.LinkSubnet(gomaasapi.LinkSubnetArgs{
+				Mode:      gomaasapi.LinkModeStatic,
+				Subnet:    subnets[subnet.Name],
+				IPAddress: subnet.Address,
+			})
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+// RenameContainer renames the MAAS device for the container without releasing any allocation
+func (c *Controller) RenameContainer(name string, newName string) error {
+	device, err := c.getDevice(name)
+	if err != nil {
+		return err
+	}
+
+	// FIXME: We should convince the Juju folks to implement an Update() method on Device
+	uri, err := url.Parse(fmt.Sprintf("%s/devices/%s/", c.url, device.SystemID()))
+	if err != nil {
+		return err
+	}
+
+	values := url.Values{}
+	values.Set("hostname", newName)
+
+	_, err = c.srvRaw.Put(uri, values)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// DeleteContainer removes the MAAS device for the container
+func (c *Controller) DeleteContainer(name string) error {
+	device, err := c.getDevice(name)
+	if err != nil {
+		return err
+	}
+
+	err = device.Delete()
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

From 8635340b25e554a04ad5447bc77cfb16bb51252b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Fri, 15 Dec 2017 01:36:19 -0500
Subject: [PATCH 2/4] Add the new MAAS configuration keys
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>
---
 config/bash/lxd-client |  3 ++-
 doc/api-extensions.md  |  6 ++++++
 doc/containers.md      | 19 +++++++++++++++++++
 doc/server.md          |  4 ++++
 lxd/container.go       |  4 ++++
 lxd/daemon_config.go   |  4 ++++
 lxd/node/config.go     |  3 +++
 shared/version/api.go  |  1 +
 8 files changed, 43 insertions(+), 1 deletion(-)

diff --git a/config/bash/lxd-client b/config/bash/lxd-client
index d7f9fb26f..e74dd8d95 100644
--- a/config/bash/lxd-client
+++ b/config/bash/lxd-client
@@ -69,7 +69,8 @@ _have lxc && {
       core.https_allowed_origin core.macaroon.endpoint core.proxy_https \
       core.proxy_http core.proxy_ignore_hosts core.trust_password \
       images.auto_update_cached images.auto_update_interval \
-      images.compression_algorithm images.remote_cache_expiry"
+      images.compression_algorithm images.remote_cache_expiry \
+      maas.api.url maas.api.key maas.machine"
 
     container_keys="boot.autostart boot.autostart.delay \
       boot.autostart.priority boot.stop.priority \
diff --git a/doc/api-extensions.md b/doc/api-extensions.md
index b49c0ed74..a6bd1ae1e 100644
--- a/doc/api-extensions.md
+++ b/doc/api-extensions.md
@@ -379,3 +379,9 @@ This adds support for optimized memory transfer during live migration.
 
 ## infiniband
 This adds support to use infiniband network devices.
+
+## maas\_network
+This adds support for MAAS network integration.
+
+When configured at the daemon level, it's then possible to attach a "nic"
+device to a particular MAAS subnet.
diff --git a/doc/containers.md b/doc/containers.md
index 2a5496be9..cfd23287b 100644
--- a/doc/containers.md
+++ b/doc/containers.md
@@ -195,6 +195,8 @@ vlan                    | integer   | -                 | no        | macvlan, p
 ipv4.address            | string    | -                 | no        | bridged                           | network                                | An IPv4 address to assign to the container through DHCP
 ipv6.address            | string    | -                 | no        | bridged                           | network                                | An IPv6 address to assign to the container through DHCP
 security.mac\_filtering | boolean   | false             | no        | bridged                           | network                                | Prevent the container from spoofing another's MAC address
+maas.subnet.ipv4        | string    | -                 | no        | bridged, macvlan, physical, sriov | maas\_network                          | MAAS IPv4 subnet to register the container in
+maas.subnet.ipv6        | string    | -                 | no        | bridged, macvlan, physical, sriov | maas\_network                          | MAAS IPv6 subnet to register the container in
 
 #### bridged or macvlan for connection to physical network
 The `bridged` and `macvlan` interface types can both be used to connect
@@ -237,6 +239,23 @@ lxc config device add <container> <device-name> nic nictype=sriov parent=<sriov-
 To tell LXD to use a specific unused VF add the `host_name` property and pass
 it the name of the enabled VF.
 
+
+#### MAAS integration
+If you're using MAAS to manage the physical network under your LXD host
+and want to attach your containers directly to a MAAS managed network,
+LXD can be configured to interact with MAAS so that it can track your
+containers.
+
+At the daemon level, you must configure `maas.api.url` and
+`maas.api.key`, then set the `maas.subnet.ipv4` and/or
+`maas.subnet.ipv6` keys on the container or profile's `nic` entry.
+
+This will have LXD register all your containers with MAAS, giving them
+proper DHCP leases and DNS records.
+
+If you set the `ipv4.address` or `ipv6.address` keys on the nic, then
+those will be registered as static assignments in MAAS too.
+
 ### Type: infiniband
 LXD supports two different kind of network types for infiniband devices:
 
diff --git a/doc/server.md b/doc/server.md
index ab565b7c0..8426246ab 100644
--- a/doc/server.md
+++ b/doc/server.md
@@ -6,6 +6,7 @@ currently supported:
 
  - `core` (core daemon configuration)
  - `images` (image configuration)
+ - `maas` (MAAS integration)
 
 Key                             | Type      | Default   | API extension            | Description
 :--                             | :---      | :------   | :------------            | :----------
@@ -23,6 +24,9 @@ images.auto\_update\_cached     | boolean   | true      | -
 images.auto\_update\_interval   | integer   | 6         | -                        | Interval in hours at which to look for update to cached images (0 disables it)
 images.compression\_algorithm   | string    | gzip      | -                        | Compression algorithm to use for new images (bzip2, gzip, lzma, xz or none)
 images.remote\_cache\_expiry    | integer   | 10        | -                        | Number of days after which an unused cached remote image will be flushed
+maas.api.key                    | string    | -         | maas\_network            | API key to manage MAAS
+maas.api.url                    | string    | -         | maas\_network            | URL of the MAAS server
+maas.machine                    | string    | hostname  | maas\_network            | Name of this LXD host in MAAS
 
 Those keys can be set using the lxc tool with:
 
diff --git a/lxd/container.go b/lxd/container.go
index 9b9d89258..53b4db3c3 100644
--- a/lxd/container.go
+++ b/lxd/container.go
@@ -135,6 +135,10 @@ func containerValidDeviceConfigKey(t, k string) bool {
 			return true
 		case "security.mac_filtering":
 			return true
+		case "maas.subnet.ipv4":
+			return true
+		case "maas.subnet.ipv6":
+			return true
 		default:
 			return false
 		}
diff --git a/lxd/daemon_config.go b/lxd/daemon_config.go
index c0b7fa722..79a80b4a8 100644
--- a/lxd/daemon_config.go
+++ b/lxd/daemon_config.go
@@ -196,6 +196,10 @@ func daemonConfigInit(db *sql.DB) error {
 		"images.compression_algorithm": {valueType: "string", validator: daemonConfigValidateCompression, defaultValue: "gzip"},
 		"images.remote_cache_expiry":   {valueType: "int", defaultValue: "10", trigger: daemonConfigTriggerExpiry},
 
+		"maas.api.key": {valueType: "string"},
+		"maas.api.url": {valueType: "string"},
+		"maas.machine": {valueType: "string"},
+
 		// Keys deprecated since the implementation of the storage api.
 		"storage.lvm_fstype":           {valueType: "string", defaultValue: "ext4", validValues: []string{"btrfs", "ext4", "xfs"}, validator: storageDeprecatedKeys},
 		"storage.lvm_mount_options":    {valueType: "string", defaultValue: "discard", validator: storageDeprecatedKeys},
diff --git a/lxd/node/config.go b/lxd/node/config.go
index 25f3d1f63..7aa6371ba 100644
--- a/lxd/node/config.go
+++ b/lxd/node/config.go
@@ -91,6 +91,9 @@ var ConfigSchema = config.Schema{
 	"images.auto_update_interval":    {},
 	"images.compression_algorithm":   {},
 	"images.remote_cache_expiry":     {},
+	"maas.api.key":                   {},
+	"maas.api.url":                   {},
+	"maas.machine":                   {},
 	"storage.lvm_fstype":             {},
 	"storage.lvm_mount_options":      {},
 	"storage.lvm_thinpool_name":      {},
diff --git a/shared/version/api.go b/shared/version/api.go
index 07bfdd9ea..eae184c59 100644
--- a/shared/version/api.go
+++ b/shared/version/api.go
@@ -84,4 +84,5 @@ var APIExtensions = []string{
 	"restrict_devlxd",
 	"migration_pre_copy",
 	"infiniband",
+	"maas_network",
 }

From 4ecc1523f2f4bfe7979f53304e7ca145df7129fe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Fri, 15 Dec 2017 02:37:55 -0500
Subject: [PATCH 3/4] lxd: Integrate MAAS controller with daemon
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        | 41 ++++++++++++++++++++++++++++++++++++++++-
 lxd/daemon_config.go | 30 +++++++++++++++++++++++++++---
 lxd/state/state.go   | 13 ++++++++-----
 3 files changed, 75 insertions(+), 9 deletions(-)

diff --git a/lxd/daemon.go b/lxd/daemon.go
index 3a22932cb..5262cdce3 100644
--- a/lxd/daemon.go
+++ b/lxd/daemon.go
@@ -27,6 +27,7 @@ import (
 
 	"github.com/lxc/lxd/lxd/db"
 	"github.com/lxc/lxd/lxd/endpoints"
+	"github.com/lxc/lxd/lxd/maas"
 	"github.com/lxc/lxd/lxd/state"
 	"github.com/lxc/lxd/lxd/sys"
 	"github.com/lxc/lxd/lxd/task"
@@ -43,6 +44,7 @@ type Daemon struct {
 	clientCerts  []x509.Certificate
 	os           *sys.OS
 	db           *db.Node
+	maas         *maas.Controller
 	readyChan    chan bool
 	shutdownChan chan bool
 
@@ -172,7 +174,7 @@ func isJSONRequest(r *http.Request) bool {
 
 // State creates a new State instance liked to our internal db and os.
 func (d *Daemon) State() *state.State {
-	return state.NewState(d.db, d.os)
+	return state.NewState(d.db, d.maas, d.os)
 }
 
 // UnixSocket returns the full path to the unix.socket file that this daemon is
@@ -418,6 +420,14 @@ func (d *Daemon) init() error {
 		return err
 	}
 
+	err = d.setupMAASController(
+		daemonConfig["maas.api.url"].Get(),
+		daemonConfig["maas.api.key"].Get(),
+		daemonConfig["maas.machine"].Get())
+	if err != nil {
+		return err
+	}
+
 	/* Setup the web server */
 	certInfo, err := shared.KeyPairAndCA(d.os.VarDir, "server", shared.CertServer)
 	if err != nil {
@@ -587,6 +597,35 @@ func (d *Daemon) setupExternalAuthentication(authEndpoint string) error {
 	return nil
 }
 
+// Setup MAAS
+func (d *Daemon) setupMAASController(server string, key string, machine string) error {
+	var err error
+	d.maas = nil
+
+	// Default the machine name to the hostname
+	if machine == "" {
+		machine, err = os.Hostname()
+		if err != nil {
+			return err
+		}
+	}
+
+	// We need both URL and key, otherwise disable MAAS
+	if server == "" || key == "" {
+		return nil
+	}
+
+	// Get a new controller struct
+	controller, err := maas.NewController(server, key, machine)
+	if err != nil {
+		d.maas = nil
+		return err
+	}
+
+	d.maas = controller
+	return nil
+}
+
 // Create a database connection and perform any updates needed.
 func initializeDbObject(d *Daemon) error {
 	// NOTE: we use the legacyPatches parameter to run a few
diff --git a/lxd/daemon_config.go b/lxd/daemon_config.go
index 79a80b4a8..741e9a624 100644
--- a/lxd/daemon_config.go
+++ b/lxd/daemon_config.go
@@ -196,9 +196,9 @@ func daemonConfigInit(db *sql.DB) error {
 		"images.compression_algorithm": {valueType: "string", validator: daemonConfigValidateCompression, defaultValue: "gzip"},
 		"images.remote_cache_expiry":   {valueType: "int", defaultValue: "10", trigger: daemonConfigTriggerExpiry},
 
-		"maas.api.key": {valueType: "string"},
-		"maas.api.url": {valueType: "string"},
-		"maas.machine": {valueType: "string"},
+		"maas.api.key": {valueType: "string", setter: daemonConfigSetMAAS},
+		"maas.api.url": {valueType: "string", setter: daemonConfigSetMAAS},
+		"maas.machine": {valueType: "string", setter: daemonConfigSetMAAS},
 
 		// Keys deprecated since the implementation of the storage api.
 		"storage.lvm_fstype":           {valueType: "string", defaultValue: "ext4", validValues: []string{"btrfs", "ext4", "xfs"}, validator: storageDeprecatedKeys},
@@ -319,6 +319,30 @@ func daemonConfigSetProxy(d *Daemon, key string, value string) (string, error) {
 	return value, nil
 }
 
+func daemonConfigSetMAAS(d *Daemon, key string, value string) (string, error) {
+	maasUrl := daemonConfig["maas.api.url"].Get()
+	if key == "maas.api.url" {
+		maasUrl = value
+	}
+
+	maasKey := daemonConfig["maas.api.key"].Get()
+	if key == "maas.api.key" {
+		maasKey = value
+	}
+
+	maasMachine := daemonConfig["maas.machine"].Get()
+	if key == "maas.machine" {
+		maasMachine = value
+	}
+
+	err := d.setupMAASController(maasUrl, maasKey, maasMachine)
+	if err != nil {
+		return "", err
+	}
+
+	return value, nil
+}
+
 func daemonConfigTriggerExpiry(d *Daemon, key string, value string) {
 	// Trigger an image pruning run
 	d.taskPruneImages.Reset()
diff --git a/lxd/state/state.go b/lxd/state/state.go
index 62b0afd72..aad9d22b5 100644
--- a/lxd/state/state.go
+++ b/lxd/state/state.go
@@ -2,6 +2,7 @@ package state
 
 import (
 	"github.com/lxc/lxd/lxd/db"
+	"github.com/lxc/lxd/lxd/maas"
 	"github.com/lxc/lxd/lxd/sys"
 )
 
@@ -9,15 +10,17 @@ import (
 // and the operating system. It's typically used by model entities such as
 // containers, volumes, etc. in order to perform changes.
 type State struct {
-	DB *db.Node
-	OS *sys.OS
+	DB   *db.Node
+	MAAS *maas.Controller
+	OS   *sys.OS
 }
 
 // NewState returns a new State object with the given database and operating
 // system components.
-func NewState(db *db.Node, os *sys.OS) *State {
+func NewState(db *db.Node, maas *maas.Controller, os *sys.OS) *State {
 	return &State{
-		DB: db,
-		OS: os,
+		DB:   db,
+		MAAS: maas,
+		OS:   os,
 	}
 }

From 36bb549421eefb6c89c4a001be72c1c51bd9509b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Mon, 18 Dec 2017 23:21:09 -0500
Subject: [PATCH 4/4] lxd/containers: Integrate MAAS with containers
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 | 180 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 180 insertions(+)

diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index e2a89b9c1..bed1fb40f 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -25,6 +25,7 @@ import (
 	"gopkg.in/yaml.v2"
 
 	"github.com/lxc/lxd/lxd/db"
+	"github.com/lxc/lxd/lxd/maas"
 	"github.com/lxc/lxd/lxd/state"
 	"github.com/lxc/lxd/lxd/types"
 	"github.com/lxc/lxd/lxd/util"
@@ -430,6 +431,14 @@ func containerLXCCreate(s *state.State, args db.ContainerArgs) (container, error
 		return nil, err
 	}
 
+	// Update MAAS
+	err = c.maasUpdate(false)
+	if err != nil {
+		c.Delete()
+		logger.Error("Failed creating container", ctxMap)
+		return nil, err
+	}
+
 	// Update lease files
 	networkUpdateStatic(s, "")
 
@@ -3093,6 +3102,13 @@ func (c *containerLXC) Delete() error {
 				}
 			}
 		}
+
+		// Delete the MAAS entry
+		err = c.maasDelete()
+		if err != nil {
+			logger.Error("Failed deleting container MAAS record", log.Ctx{"name": c.Name(), "err": err})
+			return err
+		}
 	}
 
 	// Remove the database record
@@ -3163,6 +3179,12 @@ func (c *containerLXC) Rename(newName string) error {
 	// Clean things up
 	c.cleanup()
 
+	// Rename the MAAS entry
+	err = c.maasRename(newName)
+	if err != nil {
+		return err
+	}
+
 	// Rename the logging path
 	os.RemoveAll(shared.LogPath(newName))
 	if shared.PathExists(c.LogPath()) {
@@ -3719,6 +3741,22 @@ func (c *containerLXC) Update(args db.ContainerArgs, userRequested bool) error {
 		}
 	}
 
+	// Update MAAS
+	updateMAAS := false
+	for _, key := range []string{"maas.subnet.ipv4", "maas.subnet.ipv6", "ipv4.address", "ipv6.address"} {
+		if shared.StringInSlice(key, updateDiff) {
+			updateMAAS = true
+			break
+		}
+	}
+
+	if updateMAAS {
+		err = c.maasUpdate(true)
+		if err != nil {
+			return err
+		}
+	}
+
 	// Apply the live changes
 	if isRunning {
 		// Live update the container config
@@ -7730,3 +7768,145 @@ func (c *containerLXC) StoragePool() (string, error) {
 
 	return poolName, nil
 }
+
+func (c *containerLXC) maasInterfaces() ([]maas.ContainerInterface, error) {
+	interfaces := []maas.ContainerInterface{}
+	for k, m := range c.expandedDevices {
+		if m["type"] != "nic" {
+			continue
+		}
+
+		if m["maas.subnet.ipv4"] == "" && m["maas.subnet.ipv6"] == "" {
+			continue
+		}
+
+		m, err := c.fillNetworkDevice(k, m)
+		if err != nil {
+			return nil, err
+		}
+
+		subnets := []maas.ContainerInterfaceSubnet{}
+
+		// IPv4
+		if m["maas.subnet.ipv4"] != "" {
+			subnet := maas.ContainerInterfaceSubnet{
+				Name:    m["maas.subnet.ipv4"],
+				Address: m["ipv4.address"],
+			}
+
+			subnets = append(subnets, subnet)
+		}
+
+		// IPv6
+		if m["maas.subnet.ipv6"] != "" {
+			subnet := maas.ContainerInterfaceSubnet{
+				Name:    m["maas.subnet.ipv6"],
+				Address: m["ipv6.address"],
+			}
+
+			subnets = append(subnets, subnet)
+		}
+
+		iface := maas.ContainerInterface{
+			Name:       m["name"],
+			MACAddress: m["hwaddr"],
+			Subnets:    subnets,
+		}
+
+		interfaces = append(interfaces, iface)
+	}
+
+	return interfaces, nil
+}
+
+func (c *containerLXC) maasConnected() bool {
+	for _, m := range c.expandedDevices {
+		if m["type"] != "nic" {
+			continue
+		}
+
+		if m["maas.subnet.ipv4"] != "" || m["maas.subnet.ipv6"] != "" {
+			return true
+		}
+	}
+
+	return false
+}
+
+func (c *containerLXC) maasUpdate(force bool) error {
+	if c.state.MAAS == nil {
+		return nil
+	}
+
+	if !c.maasConnected() {
+		if force {
+			exists, err := c.state.MAAS.DefinedContainer(c.name)
+			if err != nil {
+				return err
+			}
+
+			if exists {
+				return c.state.MAAS.DeleteContainer(c.name)
+			}
+		}
+		return nil
+	}
+
+	interfaces, err := c.maasInterfaces()
+	if err != nil {
+		return err
+	}
+
+	exists, err := c.state.MAAS.DefinedContainer(c.name)
+	if err != nil {
+		return err
+	}
+
+	if exists {
+		return c.state.MAAS.UpdateContainer(c.name, interfaces)
+	}
+
+	return c.state.MAAS.CreateContainer(c.name, interfaces)
+}
+
+func (c *containerLXC) maasRename(newName string) error {
+	if c.state.MAAS == nil {
+		return nil
+	}
+
+	if !c.maasConnected() {
+		return nil
+	}
+
+	exists, err := c.state.MAAS.DefinedContainer(c.name)
+	if err != nil {
+		return err
+	}
+
+	if !exists {
+		return c.maasUpdate(false)
+	}
+
+	return c.state.MAAS.RenameContainer(c.name, newName)
+}
+
+func (c *containerLXC) maasDelete() error {
+	if c.state.MAAS == nil {
+		return nil
+	}
+
+	if !c.maasConnected() {
+		return nil
+	}
+
+	exists, err := c.state.MAAS.DefinedContainer(c.name)
+	if err != nil {
+		return err
+	}
+
+	if !exists {
+		return nil
+	}
+
+	return c.state.MAAS.DeleteContainer(c.name)
+}


More information about the lxc-devel mailing list