[lxc-devel] [lxd/master] Various bugfix and minor features

stgraber on Github lxc-bot at linuxcontainers.org
Tue Feb 14 22:26:52 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/20170214/3d0d46ef/attachment.bin>
-------------- next part --------------
From b407d52b250282f92ace201c01979aa39c84bbfb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Mon, 13 Feb 2017 21:18:23 -0500
Subject: [PATCH 01/11] tests: Fix typo
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>
---
 test/main.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/main.sh b/test/main.sh
index 66585ca..f6aad33 100755
--- a/test/main.sh
+++ b/test/main.sh
@@ -455,7 +455,7 @@ fi
 run_test test_check_deps "checking dependencies"
 run_test test_static_analysis "static analysis"
 run_test test_database_update "database schema updates"
-run_test test_remote_url "remote  url handling"
+run_test test_remote_url "remote url handling"
 run_test test_remote_admin "remote administration"
 run_test test_remote_usage "remote usage"
 run_test test_basic_usage "basic usage"

From fa35a9d9eaa0fb45f67b23636bb244902ce7df82 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 14 Feb 2017 11:27:48 -0500
Subject: [PATCH 02/11] Don't include spaces in translated strings
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/info.go |  8 ++++----
 po/lxd.pot  | 34 +++++++++++++++++-----------------
 2 files changed, 21 insertions(+), 21 deletions(-)

diff --git a/lxc/info.go b/lxc/info.go
index 2300ce0..31f46f2 100644
--- a/lxc/info.go
+++ b/lxc/info.go
@@ -140,7 +140,7 @@ func (c *infoCmd) containerInfo(d *lxd.Client, name string, showLog bool) error
 		}
 
 		if diskInfo != "" {
-			fmt.Println(i18n.G("  Disk usage:"))
+			fmt.Println(fmt.Sprintf("  %s", i18n.G("Disk usage:")))
 			fmt.Printf(diskInfo)
 		}
 
@@ -151,7 +151,7 @@ func (c *infoCmd) containerInfo(d *lxd.Client, name string, showLog bool) error
 		}
 
 		if cpuInfo != "" {
-			fmt.Println(i18n.G("  CPU usage:"))
+			fmt.Println(fmt.Sprintf("  %s", i18n.G("CPU usage:")))
 			fmt.Printf(cpuInfo)
 		}
 
@@ -174,7 +174,7 @@ func (c *infoCmd) containerInfo(d *lxd.Client, name string, showLog bool) error
 		}
 
 		if memoryInfo != "" {
-			fmt.Println(i18n.G("  Memory usage:"))
+			fmt.Println(fmt.Sprintf("  %s", i18n.G("Memory usage:")))
 			fmt.Printf(memoryInfo)
 		}
 
@@ -191,7 +191,7 @@ func (c *infoCmd) containerInfo(d *lxd.Client, name string, showLog bool) error
 		}
 
 		if networkInfo != "" {
-			fmt.Println(i18n.G("  Network usage:"))
+			fmt.Println(fmt.Sprintf("  %s", i18n.G("Network usage:")))
 			fmt.Printf(networkInfo)
 		}
 	}
diff --git a/po/lxd.pot b/po/lxd.pot
index e735b8f..f8106af 100644
--- a/po/lxd.pot
+++ b/po/lxd.pot
@@ -7,7 +7,7 @@
 msgid   ""
 msgstr  "Project-Id-Version: lxd\n"
         "Report-Msgid-Bugs-To: lxc-devel at lists.linuxcontainers.org\n"
-        "POT-Creation-Date: 2017-02-14 01:13+0100\n"
+        "POT-Creation-Date: 2017-02-14 16:58-0500\n"
         "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
         "Last-Translator: FULL NAME <EMAIL at ADDRESS>\n"
         "Language-Team: LANGUAGE <LL at li.org>\n"
@@ -16,22 +16,6 @@ msgstr  "Project-Id-Version: lxd\n"
         "Content-Type: text/plain; charset=CHARSET\n"
         "Content-Transfer-Encoding: 8bit\n"
 
-#: lxc/info.go:154
-msgid   "  CPU usage:"
-msgstr  ""
-
-#: lxc/info.go:143
-msgid   "  Disk usage:"
-msgstr  ""
-
-#: lxc/info.go:177
-msgid   "  Memory usage:"
-msgstr  ""
-
-#: lxc/info.go:194
-msgid   "  Network usage:"
-msgstr  ""
-
 #: lxc/storage.go:33
 msgid   "### This is a yaml representation of a storage pool.\n"
         "### Any line starting with a '# will be ignored.\n"
@@ -197,6 +181,10 @@ msgstr  ""
 msgid   "CPU usage (in seconds)"
 msgstr  ""
 
+#: lxc/info.go:154
+msgid   "CPU usage:"
+msgstr  ""
+
 #: lxc/list.go:429
 msgid   "CREATED AT"
 msgstr  ""
@@ -356,6 +344,10 @@ msgstr  ""
 msgid   "Device %s removed from %s"
 msgstr  ""
 
+#: lxc/info.go:143
+msgid   "Disk usage:"
+msgstr  ""
+
 #: lxc/list.go:576
 msgid   "EPHEMERAL"
 msgstr  ""
@@ -877,6 +869,10 @@ msgstr  ""
 msgid   "Memory (peak)"
 msgstr  ""
 
+#: lxc/info.go:177
+msgid   "Memory usage:"
+msgstr  ""
+
 #: lxc/help.go:87
 msgid   "Missing summary."
 msgstr  ""
@@ -947,6 +943,10 @@ msgstr  ""
 msgid   "Network name"
 msgstr  ""
 
+#: lxc/info.go:194
+msgid   "Network usage:"
+msgstr  ""
+
 #: lxc/image.go:168 lxc/publish.go:35
 msgid   "New alias to define at target"
 msgstr  ""

From b209a944e950b6fb72ae7aa755aa711d8b6a8bd5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 14 Feb 2017 11:41:34 -0500
Subject: [PATCH 03/11] tests: Add golint
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>
---
 Makefile                       | 1 +
 test/suites/static_analysis.sh | 9 ++++++---
 2 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/Makefile b/Makefile
index 3970a11..5d092ed 100644
--- a/Makefile
+++ b/Makefile
@@ -48,6 +48,7 @@ protobuf:
 check: default
 	go get -v -x github.com/rogpeppe/godeps
 	go get -v -x github.com/remyoudompheng/go-misc/deadcode
+	go get -v -x github.com/golang/lint/golint
 	go test -v $(TAGS) $(DEBUG) ./...
 	cd test && ./main.sh
 
diff --git a/test/suites/static_analysis.sh b/test/suites/static_analysis.sh
index 878bce7..f17ade4 100644
--- a/test/suites/static_analysis.sh
+++ b/test/suites/static_analysis.sh
@@ -25,9 +25,7 @@ test_static_analysis() {
     fi
 
     ## go vet, if it exists
-    have_go_vet=1
-    go help vet > /dev/null 2>&1 || have_go_vet=0
-    if [ "${have_go_vet}" -eq 1 ]; then
+    if go help vet >/dev/null 2>&1; then
       go vet ./...
     fi
 
@@ -36,6 +34,11 @@ test_static_analysis() {
       vet --all .
     fi
 
+    ## golint
+    if which golint >/dev/null 2>&1; then
+      golint -set_exit_status shared/api/
+    fi
+
     ## deadcode
     if which deadcode >/dev/null 2>&1; then
       for path in . fuidshift lxc lxd lxd/types shared shared/api shared/i18n shared/ioprogress shared/logging shared/osarch shared/simplestreams shared/termios shared/version test/lxd-benchmark; do

From cbbed2577ed8b19c2345a0132650d55b1a8812c9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 14 Feb 2017 14:27:22 -0500
Subject: [PATCH 04/11] Use a tmpfs for shmounts
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Avoids some information leakage from the host.

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/daemon.go  | 21 +++++++--------------
 shared/util.go | 36 ------------------------------------
 2 files changed, 7 insertions(+), 50 deletions(-)

diff --git a/lxd/daemon.go b/lxd/daemon.go
index 4509703..f37bdb0 100644
--- a/lxd/daemon.go
+++ b/lxd/daemon.go
@@ -402,35 +402,28 @@ var sharedMounted bool
 var sharedMountsLock sync.Mutex
 
 func setupSharedMounts() error {
+	// Check if we already went through this
 	if sharedMounted {
 		return nil
 	}
 
+	// Get a lock to prevent races
 	sharedMountsLock.Lock()
 	defer sharedMountsLock.Unlock()
 
-	if sharedMounted {
-		return nil
-	}
-
+	// Check if already setup
 	path := shared.VarPath("shmounts")
-
-	isShared, err := shared.IsOnSharedMount(path)
-	if err != nil {
-		return err
-	}
-
-	if isShared {
-		// / may already be ms-shared, or shmounts may have
-		// been mounted by a previous lxd run
+	if shared.IsMountPoint(path) {
 		sharedMounted = true
 		return nil
 	}
 
-	if err := syscall.Mount(path, path, "none", syscall.MS_BIND, ""); err != nil {
+	// Mount a new tmpfs
+	if err := syscall.Mount("tmpfs", path, "tmpfs", 0, "size=100k,mode=0711"); err != nil {
 		return err
 	}
 
+	// Mark as MS_SHARED and MS_REC
 	var flags uintptr = syscall.MS_SHARED | syscall.MS_REC
 	if err := syscall.Mount(path, path, "none", flags, ""); err != nil {
 		return err
diff --git a/shared/util.go b/shared/util.go
index 0474e4d..69f83ac 100644
--- a/shared/util.go
+++ b/shared/util.go
@@ -430,42 +430,6 @@ func IsTrue(value string) bool {
 	return false
 }
 
-func IsOnSharedMount(pathName string) (bool, error) {
-	file, err := os.Open("/proc/self/mountinfo")
-	if err != nil {
-		return false, err
-	}
-	defer file.Close()
-
-	absPath, err := filepath.Abs(pathName)
-	if err != nil {
-		return false, err
-	}
-
-	expPath, err := os.Readlink(absPath)
-	if err != nil {
-		expPath = absPath
-	}
-
-	scanner := bufio.NewScanner(file)
-	for scanner.Scan() {
-		line := scanner.Text()
-		rows := strings.Fields(line)
-
-		if rows[4] != expPath {
-			continue
-		}
-
-		if strings.HasPrefix(rows[6], "shared:") {
-			return true, nil
-		} else {
-			return false, nil
-		}
-	}
-
-	return false, nil
-}
-
 func IsBlockdev(fm os.FileMode) bool {
 	return ((fm&os.ModeDevice != 0) && (fm&os.ModeCharDevice == 0))
 }

From 17a4ad1a240a78fd82013101466311f5080af4f0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 14 Feb 2017 15:01:10 -0500
Subject: [PATCH 05/11] Mount a tmpfs under devlxd
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Closes #2877

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/daemon.go | 26 +++++++++++++++++++-------
 1 file changed, 19 insertions(+), 7 deletions(-)

diff --git a/lxd/daemon.go b/lxd/daemon.go
index f37bdb0..2de8487 100644
--- a/lxd/daemon.go
+++ b/lxd/daemon.go
@@ -854,6 +854,17 @@ func (d *Daemon) Init() error {
 		daemonConfig["core.proxy_ignore_hosts"].Get(),
 	)
 
+	/* Setup some mounts (nice to have) */
+	if !d.MockMode {
+		// Attempt to mount the shmounts tmpfs
+		setupSharedMounts()
+
+		// Attempt to Mount the devlxd tmpfs
+		if !shared.IsMountPoint(shared.VarPath("devlxd")) {
+			syscall.Mount("tmpfs", shared.VarPath("devlxd"), "tmpfs", 0, "size=100k,mode=0755")
+		}
+	}
+
 	/* Setup /dev/lxd */
 	shared.LogInfof("Starting /dev/lxd handler")
 	d.devlxd, err = createAndBindDevLxd()
@@ -1161,23 +1172,24 @@ func (d *Daemon) Stop() error {
 		}
 	}
 
+	shared.LogInfof("Stopping /dev/lxd handler")
+	d.devlxd.Close()
+	shared.LogInfof("Stopped /dev/lxd handler")
+
 	if n, err := d.numRunningContainers(); err != nil || n == 0 {
-		shared.LogInfof("Unmounting shmounts")
+		shared.LogInfof("Unmounting temporary filesystems")
 
+		syscall.Unmount(shared.VarPath("devlxd"), syscall.MNT_DETACH)
 		syscall.Unmount(shared.VarPath("shmounts"), syscall.MNT_DETACH)
 
-		shared.LogInfof("Done unmounting shmounts")
+		shared.LogInfof("Done unmounting temporary filesystems")
 	} else {
-		shared.LogDebugf("Not unmounting shmounts (containers are still running)")
+		shared.LogDebugf("Not unmounting temporary filesystems (containers are still running)")
 	}
 
 	shared.LogInfof("Closing the database")
 	d.db.Close()
 
-	shared.LogInfof("Stopping /dev/lxd handler")
-	d.devlxd.Close()
-	shared.LogInfof("Stopped /dev/lxd handler")
-
 	shared.LogInfof("Saving simplestreams cache")
 	imageSaveStreamCache()
 	shared.LogInfof("Saved simplestreams cache")

From c77f2052231fc4b775df742289b644e2234f23f6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 14 Feb 2017 15:27:03 -0500
Subject: [PATCH 06/11] Allow setting network interface name on attach
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Closes #2873

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxc/network.go | 16 ++++++++++++----
 po/lxd.pot     | 38 +++++++++++++++++++-------------------
 2 files changed, 31 insertions(+), 23 deletions(-)

diff --git a/lxc/network.go b/lxc/network.go
index 0966173..9e890d7 100644
--- a/lxc/network.go
+++ b/lxc/network.go
@@ -61,8 +61,8 @@ lxc network edit [<remote>:]<network>
     Example: lxc network edit <network> # launch editor
              cat network.yaml | lxc network edit <network> # read from network.yaml
 
-lxc network attach [<remote>:]<network> <container> [device name]
-lxc network attach-profile [<remote>:]<network> <profile> [device name]
+lxc network attach [<remote>:]<network> <container> [device name] [interface name]
+lxc network attach-profile [<remote>:]<network> <profile> [device name] [interface name]
 
 lxc network detach [<remote>:]<network> <container> [device name]
 lxc network detach-profile [<remote>:]<network> <container> [device name]`)
@@ -118,7 +118,7 @@ func (c *networkCmd) run(config *lxd.Config, args []string) error {
 }
 
 func (c *networkCmd) doNetworkAttach(client *lxd.Client, name string, args []string) error {
-	if len(args) < 1 || len(args) > 2 {
+	if len(args) < 1 || len(args) > 3 {
 		return errArgs
 	}
 
@@ -139,6 +139,10 @@ func (c *networkCmd) doNetworkAttach(client *lxd.Client, name string, args []str
 	}
 
 	props := []string{fmt.Sprintf("nictype=%s", nicType), fmt.Sprintf("parent=%s", name)}
+	if len(args) > 2 {
+		props = append(props, fmt.Sprintf("name=%s", args[2]))
+	}
+
 	resp, err := client.ContainerDeviceAdd(container, devName, "nic", props)
 	if err != nil {
 		return err
@@ -148,7 +152,7 @@ func (c *networkCmd) doNetworkAttach(client *lxd.Client, name string, args []str
 }
 
 func (c *networkCmd) doNetworkAttachProfile(client *lxd.Client, name string, args []string) error {
-	if len(args) < 1 || len(args) > 2 {
+	if len(args) < 1 || len(args) > 3 {
 		return errArgs
 	}
 
@@ -169,6 +173,10 @@ func (c *networkCmd) doNetworkAttachProfile(client *lxd.Client, name string, arg
 	}
 
 	props := []string{fmt.Sprintf("nictype=%s", nicType), fmt.Sprintf("parent=%s", name)}
+	if len(args) > 2 {
+		props = append(props, fmt.Sprintf("name=%s", args[2]))
+	}
+
 	_, err = client.ProfileDeviceAdd(profile, devName, "nic", props)
 	return err
 }
diff --git a/po/lxd.pot b/po/lxd.pot
index f8106af..04e1782 100644
--- a/po/lxd.pot
+++ b/po/lxd.pot
@@ -189,7 +189,7 @@ msgstr  ""
 msgid   "CREATED AT"
 msgstr  ""
 
-#: lxc/config.go:114 lxc/network.go:463
+#: lxc/config.go:114 lxc/network.go:471
 #, c-format
 msgid   "Can't read from stdin: %s"
 msgstr  ""
@@ -199,7 +199,7 @@ msgstr  ""
 msgid   "Can't unset key '%s', it's not currently set."
 msgstr  ""
 
-#: lxc/network.go:390 lxc/profile.go:424 lxc/storage.go:522
+#: lxc/network.go:398 lxc/profile.go:424 lxc/storage.go:522
 msgid   "Cannot provide container name to list"
 msgstr  ""
 
@@ -233,7 +233,7 @@ msgstr  ""
 msgid   "Config key/value to apply to the new container"
 msgstr  ""
 
-#: lxc/config.go:535 lxc/config.go:600 lxc/image.go:737 lxc/network.go:346 lxc/profile.go:218 lxc/storage.go:478 lxc/storage.go:824
+#: lxc/config.go:535 lxc/config.go:600 lxc/image.go:737 lxc/network.go:354 lxc/profile.go:218 lxc/storage.go:478 lxc/storage.go:824
 #, c-format
 msgid   "Config parsing error: %s"
 msgstr  ""
@@ -617,7 +617,7 @@ msgstr  ""
 msgid   "Log:"
 msgstr  ""
 
-#: lxc/network.go:428
+#: lxc/network.go:436
 msgid   "MANAGED"
 msgstr  ""
 
@@ -739,8 +739,8 @@ msgid   "Manage networks.\n"
         "    Example: lxc network edit <network> # launch editor\n"
         "             cat network.yaml | lxc network edit <network> # read from network.yaml\n"
         "\n"
-        "lxc network attach [<remote>:]<network> <container> [device name]\n"
-        "lxc network attach-profile [<remote>:]<network> <profile> [device name]\n"
+        "lxc network attach [<remote>:]<network> <container> [device name] [interface name]\n"
+        "lxc network attach-profile [<remote>:]<network> <profile> [device name] [interface name]\n"
         "\n"
         "lxc network detach [<remote>:]<network> <container> [device name]\n"
         "lxc network detach-profile [<remote>:]<network> <container> [device name]"
@@ -891,7 +891,7 @@ msgid   "Monitor activity on the LXD server.\n"
         "    lxc monitor --type=logging"
 msgstr  ""
 
-#: lxc/network.go:216 lxc/network.go:265 lxc/storage.go:309 lxc/storage.go:405
+#: lxc/network.go:224 lxc/network.go:273 lxc/storage.go:309 lxc/storage.go:405
 msgid   "More than one device matches, specify the device name."
 msgstr  ""
 
@@ -916,11 +916,11 @@ msgstr  ""
 msgid   "Must supply container name for: "
 msgstr  ""
 
-#: lxc/list.go:431 lxc/network.go:426 lxc/profile.go:451 lxc/remote.go:380 lxc/storage.go:550 lxc/storage.go:640 lxc/storage.go:673
+#: lxc/list.go:431 lxc/network.go:434 lxc/profile.go:451 lxc/remote.go:380 lxc/storage.go:550 lxc/storage.go:640 lxc/storage.go:673
 msgid   "NAME"
 msgstr  ""
 
-#: lxc/network.go:412 lxc/remote.go:354 lxc/remote.go:359
+#: lxc/network.go:420 lxc/remote.go:354 lxc/remote.go:359
 msgid   "NO"
 msgstr  ""
 
@@ -929,12 +929,12 @@ msgstr  ""
 msgid   "Name: %s"
 msgstr  ""
 
-#: lxc/network.go:190
+#: lxc/network.go:198
 #, c-format
 msgid   "Network %s created"
 msgstr  ""
 
-#: lxc/network.go:293
+#: lxc/network.go:301
 #, c-format
 msgid   "Network %s deleted"
 msgstr  ""
@@ -955,7 +955,7 @@ msgstr  ""
 msgid   "No certificate provided to add"
 msgstr  ""
 
-#: lxc/network.go:225 lxc/network.go:274
+#: lxc/network.go:233 lxc/network.go:282
 msgid   "No device found for this network"
 msgstr  ""
 
@@ -975,7 +975,7 @@ msgstr  ""
 msgid   "Only https:// is supported for remote image import."
 msgstr  ""
 
-#: lxc/network.go:322 lxc/network.go:449
+#: lxc/network.go:330 lxc/network.go:457
 msgid   "Only managed networks can be modified."
 msgstr  ""
 
@@ -1037,7 +1037,7 @@ msgstr  ""
 msgid   "Pid: %d"
 msgstr  ""
 
-#: lxc/network.go:347 lxc/profile.go:219 lxc/storage.go:479 lxc/storage.go:825
+#: lxc/network.go:355 lxc/profile.go:219 lxc/storage.go:479 lxc/storage.go:825
 msgid   "Press enter to open the editor again"
 msgstr  ""
 
@@ -1303,7 +1303,7 @@ msgstr  ""
 msgid   "Swap (peak)"
 msgstr  ""
 
-#: lxc/list.go:436 lxc/network.go:427 lxc/storage.go:641 lxc/storage.go:674
+#: lxc/list.go:436 lxc/network.go:435 lxc/storage.go:641 lxc/storage.go:674
 msgid   "TYPE"
 msgstr  ""
 
@@ -1332,11 +1332,11 @@ msgstr  ""
 msgid   "The opposite of `lxc pause` is `lxc start`."
 msgstr  ""
 
-#: lxc/network.go:230 lxc/network.go:279 lxc/storage.go:323 lxc/storage.go:419
+#: lxc/network.go:238 lxc/network.go:287 lxc/storage.go:323 lxc/storage.go:419
 msgid   "The specified device doesn't exist"
 msgstr  ""
 
-#: lxc/network.go:234 lxc/network.go:283
+#: lxc/network.go:242 lxc/network.go:291
 msgid   "The specified device doesn't match the network"
 msgstr  ""
 
@@ -1390,7 +1390,7 @@ msgstr  ""
 msgid   "URL"
 msgstr  ""
 
-#: lxc/network.go:429 lxc/profile.go:452 lxc/storage.go:553 lxc/storage.go:642 lxc/storage.go:675
+#: lxc/network.go:437 lxc/profile.go:452 lxc/storage.go:553 lxc/storage.go:642 lxc/storage.go:675
 msgid   "USED BY"
 msgstr  ""
 
@@ -1424,7 +1424,7 @@ msgstr  ""
 msgid   "Whether or not to snapshot the container's running state"
 msgstr  ""
 
-#: lxc/network.go:414 lxc/remote.go:356 lxc/remote.go:361
+#: lxc/network.go:422 lxc/remote.go:356 lxc/remote.go:361
 msgid   "YES"
 msgstr  ""
 

From 59bbab88ec8c1f2a4c7772eba5640a72c9f7bc4b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 14 Feb 2017 16:08:46 -0500
Subject: [PATCH 07/11] Fix error handling on FileRemove
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 | 23 ++++++++++++++++++-----
 1 file changed, 18 insertions(+), 5 deletions(-)

diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index 05f252f..46600bf 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -4597,6 +4597,8 @@ func (c *containerLXC) FilePush(srcpath string, dstpath string, uid int, gid int
 }
 
 func (c *containerLXC) FileRemove(path string) error {
+	var errStr string
+
 	// Setup container storage if needed
 	if !c.IsRunning() {
 		err := c.StorageStart()
@@ -4623,13 +4625,24 @@ func (c *containerLXC) FileRemove(path string) error {
 	}
 
 	// Process forkremovefile response
-	if string(out) != "" {
-		if strings.HasPrefix(string(out), "error:") {
-			return fmt.Errorf(strings.TrimPrefix(strings.TrimSuffix(string(out), "\n"), "error: "))
+	for _, line := range strings.Split(strings.TrimRight(string(out), "\n"), "\n") {
+		if line == "" {
+			continue
 		}
 
-		for _, line := range strings.Split(strings.TrimRight(string(out), "\n"), "\n") {
-			shared.LogDebugf("forkremovefile: %s", line)
+		// Extract errors
+		if strings.HasPrefix(line, "error: ") {
+			errStr = strings.TrimPrefix(line, "error: ")
+			continue
+		}
+
+		if strings.HasPrefix(line, "errno: ") {
+			errno := strings.TrimPrefix(line, "errno: ")
+			if errno == "2" {
+				return os.ErrNotExist
+			}
+
+			return fmt.Errorf(errStr)
 		}
 	}
 

From b9d73d05bfc439c6e740fd18cb77554b499f54dc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 14 Feb 2017 16:09:03 -0500
Subject: [PATCH 08/11] Implement file DELETE
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Closes #2868

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 client.go             | 14 ++++++++++++++
 doc/api-extensions.md |  3 +++
 doc/rest-api.md       | 12 ++++++++++++
 lxc/file.go           | 29 +++++++++++++++++++++++++++++
 lxd/api_1.0.go        |  1 +
 lxd/container_file.go | 11 +++++++++++
 lxd/containers.go     |  7 ++++---
 7 files changed, 74 insertions(+), 3 deletions(-)

diff --git a/client.go b/client.go
index e013499..7232b7b 100644
--- a/client.go
+++ b/client.go
@@ -1962,6 +1962,20 @@ func (c *Client) RecursivePullFile(container string, p string, targetDir string)
 	return nil
 }
 
+func (c *Client) DeleteFile(container string, p string) error {
+	if c.Remote.Public {
+		return fmt.Errorf("This function isn't supported by public remotes.")
+	}
+
+	query := url.Values{"path": []string{p}}
+	_, err := c.delete(fmt.Sprintf("containers/%s/files?%s", container, query.Encode()), nil, api.SyncResponse)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
 func (c *Client) GetMigrationSourceWS(container string) (*api.Response, error) {
 	if c.Remote.Public {
 		return nil, fmt.Errorf("This function isn't supported by public remotes.")
diff --git a/doc/api-extensions.md b/doc/api-extensions.md
index 55a91d3..b6a9d23 100644
--- a/doc/api-extensions.md
+++ b/doc/api-extensions.md
@@ -202,3 +202,6 @@ This includes:
 * DELETE /1.0/storage-pools/<pool>/volumes/<volume_type>/<name> (see rest-api.md for details)
 
 - All storage configuration options (see configuration.md for details)
+
+## file\_delete
+Implements DELETE in /1.0/containers/\<name\>/files
diff --git a/doc/rest-api.md b/doc/rest-api.md
index 65302a1..8a07bef 100644
--- a/doc/rest-api.md
+++ b/doc/rest-api.md
@@ -819,6 +819,18 @@ The following headers may be set by the client:
 This is designed to be easily usable from the command line or even a web
 browser.
 
+### DELETE (?path=/path/inside/the/container)
+ * Description: delete a file in the container
+ * Introduced: with API extension "file\_delete"
+ * Authentication: trusted
+ * Operation: sync
+ * Return: standard return value or standard error
+
+Input (none at present):
+
+    {
+    }
+
 ## /1.0/containers/\<name\>/snapshots
 ### GET
  * Description: List of snapshots
diff --git a/lxc/file.go b/lxc/file.go
index 1fbb65c..9760bee 100644
--- a/lxc/file.go
+++ b/lxc/file.go
@@ -38,6 +38,7 @@ func (c *fileCmd) usage() string {
 
 lxc file pull [-r|--recursive] [<remote>:]<container> [[<remote>:]<container>...] <target path>
 lxc file push [-r|--recursive] [-p|--create-dirs] [--uid=UID] [--gid=GID] [--mode=MODE] <source path> [<source path>...] [<remote>:]<container>
+lxc file delete [<remote>:]<container> [[<remote>:]<container>...]
 lxc file edit [<remote>:]<container>/<path>
 
 <source> in the case of pull, <target> in the case of push and <file> in the case of edit are <container name>/<path>
@@ -340,6 +341,32 @@ func (c *fileCmd) pull(config *lxd.Config, args []string) error {
 	return nil
 }
 
+func (c *fileCmd) delete(config *lxd.Config, args []string) error {
+	if len(args) < 1 {
+		return errArgs
+	}
+
+	for _, f := range args[:len(args)] {
+		pathSpec := strings.SplitN(f, "/", 2)
+		if len(pathSpec) != 2 {
+			return fmt.Errorf(i18n.G("Invalid path %s"), f)
+		}
+
+		remote, container := config.ParseRemoteAndContainer(pathSpec[0])
+		d, err := lxd.NewClient(config, remote)
+		if err != nil {
+			return err
+		}
+
+		err = d.DeleteFile(container, pathSpec[1])
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
 func (c *fileCmd) edit(config *lxd.Config, args []string) error {
 	if len(args) != 1 {
 		return errArgs
@@ -390,6 +417,8 @@ func (c *fileCmd) run(config *lxd.Config, args []string) error {
 		return c.push(config, true, args[1:])
 	case "pull":
 		return c.pull(config, args[1:])
+	case "delete":
+		return c.delete(config, args[1:])
 	case "edit":
 		return c.edit(config, args[1:])
 	default:
diff --git a/lxd/api_1.0.go b/lxd/api_1.0.go
index 3951f94..963c815 100644
--- a/lxd/api_1.0.go
+++ b/lxd/api_1.0.go
@@ -105,6 +105,7 @@ func api10Get(d *Daemon, r *http.Request) Response {
 			"network_firewall_filtering",
 			"network_routes",
 			"storage",
+			"file_delete",
 		},
 		APIStatus:  "stable",
 		APIVersion: version.APIVersion,
diff --git a/lxd/container_file.go b/lxd/container_file.go
index 7e9d20b..a4c1714 100644
--- a/lxd/container_file.go
+++ b/lxd/container_file.go
@@ -30,6 +30,8 @@ func containerFileHandler(d *Daemon, r *http.Request) Response {
 		return containerFileGet(c, path, r)
 	case "POST":
 		return containerFilePut(c, path, r)
+	case "DELETE":
+		return containerFileDelete(c, path, r)
 	default:
 		return NotFound
 	}
@@ -117,3 +119,12 @@ func containerFilePut(c container, path string, r *http.Request) Response {
 		return InternalError(fmt.Errorf("bad file type %s", type_))
 	}
 }
+
+func containerFileDelete(c container, path string, r *http.Request) Response {
+	err := c.FileRemove(path)
+	if err != nil {
+		return SmartError(err)
+	}
+
+	return EmptySyncResponse
+}
diff --git a/lxd/containers.go b/lxd/containers.go
index d5305d3..3bb06ae 100644
--- a/lxd/containers.go
+++ b/lxd/containers.go
@@ -33,9 +33,10 @@ var containerStateCmd = Command{
 }
 
 var containerFileCmd = Command{
-	name: "containers/{name}/files",
-	get:  containerFileHandler,
-	post: containerFileHandler,
+	name:   "containers/{name}/files",
+	get:    containerFileHandler,
+	post:   containerFileHandler,
+	delete: containerFileHandler,
 }
 
 var containerSnapshotsCmd = Command{

From a3b28399a72627a29e118a128af798a67987ef7d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 14 Feb 2017 16:15:05 -0500
Subject: [PATCH 09/11] doc: Clarify PUT vs PATCH
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Closes #2873

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 doc/rest-api.md | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/doc/rest-api.md b/doc/rest-api.md
index 8a07bef..4c3f5e0 100644
--- a/doc/rest-api.md
+++ b/doc/rest-api.md
@@ -161,6 +161,21 @@ It's recommended that the client always subscribes to the operations
 notification type before triggering remote operations so that it doesn't
 have to then poll for their status.
 
+# PUT vs PATCH
+The LXD API supports both PUT and PATCH to modify existing objects.
+
+PUT replaces the entire object with a new definition, it's typically
+called after the current object state was retrieved through GET.
+
+To avoid race conditions, the Etag header should be read from the GET
+response and sent as If-Match for the PUT request. This will cause LXD
+to fail the request if the object was modified between GET and PUT.
+
+PATCH can be used to modify a single field inside an object by only
+specifying the property that you want to change. To unset a key, setting
+it to empty will usually do the trick, but there are cases where PATCH
+won't work and PUT needs to be used instead.
+
 # API structure
  * /
    * /1.0

From e33dc0715dc335d8dd8bc246a687c4abbe50e1cb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 14 Feb 2017 16:36:47 -0500
Subject: [PATCH 10/11] Make it possible to append to a file
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Closes #2871

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 client.go             |  2 +-
 doc/api-extensions.md |  3 +++
 doc/rest-api.md       |  1 +
 lxd/api_1.0.go        |  1 +
 lxd/container.go      |  2 +-
 lxd/container_file.go | 12 ++++++++----
 lxd/container_lxc.go  |  6 ++++--
 lxd/main_nsexec.go    | 25 ++++++++++++++++++-------
 shared/util.go        | 12 ++++++++++--
 9 files changed, 47 insertions(+), 17 deletions(-)

diff --git a/client.go b/client.go
index 7232b7b..6893165 100644
--- a/client.go
+++ b/client.go
@@ -1896,7 +1896,7 @@ func (c *Client) PullFile(container string, p string) (int, int, int, string, io
 		return 0, 0, 0, "", nil, nil, err
 	}
 
-	uid, gid, mode, type_ := shared.ParseLXDFileHeaders(r.Header)
+	uid, gid, mode, type_, _ := shared.ParseLXDFileHeaders(r.Header)
 	if type_ == "directory" {
 		resp, err := HoistResponse(r, api.SyncResponse)
 		if err != nil {
diff --git a/doc/api-extensions.md b/doc/api-extensions.md
index b6a9d23..9c76606 100644
--- a/doc/api-extensions.md
+++ b/doc/api-extensions.md
@@ -205,3 +205,6 @@ This includes:
 
 ## file\_delete
 Implements DELETE in /1.0/containers/\<name\>/files
+
+## file\_append
+Implements the X-LXD-write header which can be one of "overwrite" or "append".
diff --git a/doc/rest-api.md b/doc/rest-api.md
index 4c3f5e0..d883338 100644
--- a/doc/rest-api.md
+++ b/doc/rest-api.md
@@ -830,6 +830,7 @@ The following headers may be set by the client:
  * X-LXD-uid: 0
  * X-LXD-gid: 0
  * X-LXD-mode: 0700
+ * X-LXD-write: overwrite (or append, introduced with API extension "file\_append")
 
 This is designed to be easily usable from the command line or even a web
 browser.
diff --git a/lxd/api_1.0.go b/lxd/api_1.0.go
index 963c815..215f4a9 100644
--- a/lxd/api_1.0.go
+++ b/lxd/api_1.0.go
@@ -106,6 +106,7 @@ func api10Get(d *Daemon, r *http.Request) Response {
 			"network_routes",
 			"storage",
 			"file_delete",
+			"file_append",
 		},
 		APIStatus:  "stable",
 		APIVersion: version.APIVersion,
diff --git a/lxd/container.go b/lxd/container.go
index 20a41dd..e4ef23c 100644
--- a/lxd/container.go
+++ b/lxd/container.go
@@ -370,7 +370,7 @@ type container interface {
 	// File handling
 	FileExists(path string) error
 	FilePull(srcpath string, dstpath string) (int, int, os.FileMode, string, []string, error)
-	FilePush(srcpath string, dstpath string, uid int, gid int, mode int) error
+	FilePush(srcpath string, dstpath string, uid int, gid int, mode int, write string) error
 	FileRemove(path string) error
 
 	/* Command execution:
diff --git a/lxd/container_file.go b/lxd/container_file.go
index a4c1714..eb443a3 100644
--- a/lxd/container_file.go
+++ b/lxd/container_file.go
@@ -84,7 +84,11 @@ func containerFileGet(c container, path string, r *http.Request) Response {
 
 func containerFilePut(c container, path string, r *http.Request) Response {
 	// Extract file ownership and mode from headers
-	uid, gid, mode, type_ := shared.ParseLXDFileHeaders(r.Header)
+	uid, gid, mode, type_, write := shared.ParseLXDFileHeaders(r.Header)
+
+	if !shared.StringInSlice(write, []string{"overwrite", "append"}) {
+		return BadRequest(fmt.Errorf("Bad file write mode: %s", write))
+	}
 
 	if type_ == "file" {
 		// Write file content to a tempfile
@@ -103,20 +107,20 @@ func containerFilePut(c container, path string, r *http.Request) Response {
 		}
 
 		// Transfer the file into the container
-		err = c.FilePush(temp.Name(), path, uid, gid, mode)
+		err = c.FilePush(temp.Name(), path, uid, gid, mode, write)
 		if err != nil {
 			return InternalError(err)
 		}
 
 		return EmptySyncResponse
 	} else if type_ == "directory" {
-		err := c.FilePush("", path, uid, gid, mode)
+		err := c.FilePush("", path, uid, gid, mode, write)
 		if err != nil {
 			return InternalError(err)
 		}
 		return EmptySyncResponse
 	} else {
-		return InternalError(fmt.Errorf("bad file type %s", type_))
+		return BadRequest(fmt.Errorf("Bad file type: %s", type_))
 	}
 }
 
diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index 46600bf..8a62d9f 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -4505,7 +4505,7 @@ func (c *containerLXC) FilePull(srcpath string, dstpath string) (int, int, os.Fi
 	return uid, gid, os.FileMode(mode), type_, dirEnts, nil
 }
 
-func (c *containerLXC) FilePush(srcpath string, dstpath string, uid int, gid int, mode int) error {
+func (c *containerLXC) FilePush(srcpath string, dstpath string, uid int, gid int, mode int, write string) error {
 	var rootUid = 0
 	var rootGid = 0
 	var errStr string
@@ -4545,6 +4545,7 @@ func (c *containerLXC) FilePush(srcpath string, dstpath string, uid int, gid int
 		fmt.Sprintf("%d", rootUid),
 		fmt.Sprintf("%d", rootGid),
 		fmt.Sprintf("%d", int(os.FileMode(0640)&os.ModePerm)),
+		write,
 	).CombinedOutput()
 
 	// Tear down container storage if needed
@@ -4579,7 +4580,7 @@ func (c *containerLXC) FilePush(srcpath string, dstpath string, uid int, gid int
 
 	if err != nil {
 		return fmt.Errorf(
-			"Error calling 'lxd forkputfile %s %d %s %s %d %d %d %d %d %d': err='%v'",
+			"Error calling 'lxd forkputfile %s %d %s %s %d %d %d %d %d %d %s': err='%v'",
 			c.RootfsPath(),
 			c.InitPID(),
 			srcpath,
@@ -4590,6 +4591,7 @@ func (c *containerLXC) FilePush(srcpath string, dstpath string, uid int, gid int
 			rootUid,
 			rootGid,
 			int(os.FileMode(0640)&os.ModePerm),
+			write,
 			err)
 	}
 
diff --git a/lxd/main_nsexec.go b/lxd/main_nsexec.go
index a1239f4..67c3c14 100644
--- a/lxd/main_nsexec.go
+++ b/lxd/main_nsexec.go
@@ -87,16 +87,21 @@ int mkdir_p(const char *dir, mode_t mode)
 	return 0;
 }
 
-int copy(int target, int source)
+int copy(int target, int source, bool append)
 {
 	ssize_t n;
 	char buf[1024];
 
-	if (ftruncate(target, 0) < 0) {
+	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");
@@ -175,7 +180,7 @@ void attach_userns(int pid) {
 	}
 }
 
-int manip_file_in_ns(char *rootfs, int pid, char *host, char *container, bool is_put, uid_t uid, gid_t gid, mode_t mode, uid_t defaultUid, gid_t defaultGid, mode_t defaultMode) {
+int manip_file_in_ns(char *rootfs, int pid, char *host, char *container, bool is_put, 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;
@@ -273,7 +278,7 @@ int manip_file_in_ns(char *rootfs, int pid, char *host, char *container, bool is
 			}
 		}
 
-		if (copy(container_fd, host_fd) < 0) {
+		if (copy(container_fd, host_fd, append) < 0) {
 			error("error: copy");
 			goto close_container;
 		}
@@ -333,7 +338,7 @@ int manip_file_in_ns(char *rootfs, int pid, char *host, char *container, bool is
 			goto close_host;
 		} else {
 			fprintf(stderr, "type: file\n");
-			ret = copy(host_fd, container_fd);
+			ret = copy(host_fd, container_fd, false);
 		}
 		fprintf(stderr, "type: %s", S_ISDIR(st.st_mode) ? "directory" : "file");
 	}
@@ -497,8 +502,9 @@ void forkdofile(char *buf, char *cur, bool is_put, ssize_t size) {
 	uid_t defaultUid = 0;
 	gid_t defaultGid = 0;
 	mode_t defaultMode = 0;
-	char *command = cur, *rootfs = NULL, *source = NULL, *target = NULL;
+	char *command = cur, *rootfs = NULL, *source = NULL, *target = NULL, *writeMode = NULL;
 	pid_t pid;
+	bool append = false;
 
 	ADVANCE_ARG_REQUIRED();
 	rootfs = cur;
@@ -530,9 +536,14 @@ void forkdofile(char *buf, char *cur, bool is_put, ssize_t size) {
 
 		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, uid, gid, mode, defaultUid, defaultGid, defaultMode));
+	_exit(manip_file_in_ns(rootfs, pid, source, target, is_put, uid, gid, mode, defaultUid, defaultGid, defaultMode, append));
 }
 
 void forkcheckfile(char *buf, char *cur, bool is_put, ssize_t size) {
diff --git a/shared/util.go b/shared/util.go
index 69f83ac..1e0b878 100644
--- a/shared/util.go
+++ b/shared/util.go
@@ -123,7 +123,7 @@ func LogPath(path ...string) string {
 	return filepath.Join(items...)
 }
 
-func ParseLXDFileHeaders(headers http.Header) (uid int, gid int, mode int, type_ string) {
+func ParseLXDFileHeaders(headers http.Header) (uid int, gid int, mode int, type_ string, write string) {
 	uid, err := strconv.Atoi(headers.Get("X-LXD-uid"))
 	if err != nil {
 		uid = -1
@@ -152,7 +152,15 @@ func ParseLXDFileHeaders(headers http.Header) (uid int, gid int, mode int, type_
 		type_ = "file"
 	}
 
-	return uid, gid, mode, type_
+	write = headers.Get("X-LXD-write")
+	/* backwards compat: before "write" was introduced, we could only
+	 * overwrite files
+	 */
+	if write == "" {
+		write = "overwrite"
+	}
+
+	return uid, gid, mode, type_, write
 }
 
 func ReadToJSON(r io.Reader, req interface{}) error {

From a9e04686f712303d691afab8463df1675048f439 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 14 Feb 2017 16:57:55 -0500
Subject: [PATCH 11/11] network: Implement configurable DHCP lease time
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Closes #2835

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 doc/api-extensions.md  |  3 +++
 doc/configuration.md   |  2 ++
 lxd/api_1.0.go         |  1 +
 lxd/networks.go        | 18 ++++++++++++++----
 lxd/networks_config.go |  2 ++
 5 files changed, 22 insertions(+), 4 deletions(-)

diff --git a/doc/api-extensions.md b/doc/api-extensions.md
index 9c76606..dbf18c6 100644
--- a/doc/api-extensions.md
+++ b/doc/api-extensions.md
@@ -208,3 +208,6 @@ Implements DELETE in /1.0/containers/\<name\>/files
 
 ## file\_append
 Implements the X-LXD-write header which can be one of "overwrite" or "append".
+
+## network\_dhcp\_expiry
+Introduces "ipv4.dhcp.expiry" and "ipv6.dhcp.expiry" allowing to set the DHCP lease expiry time.
diff --git a/doc/configuration.md b/doc/configuration.md
index 981647e..b791fbd 100644
--- a/doc/configuration.md
+++ b/doc/configuration.md
@@ -355,6 +355,7 @@ tunnel.NAME.id                  | integer   | vxlan                 | 0
 ipv4.address                    | string    | standard mode         | random unused subnet      | IPv4 address for the bridge (CIDR notation). Use "none" to turn off IPv4 or "auto" to generate a new one
 ipv4.nat                        | boolean   | ipv4 address          | false                     | Whether to NAT (will default to true if unset and a random ipv4.address is generated)
 ipv4.dhcp                       | boolean   | ipv4 address          | true                      | Whether to allocate addresses using DHCP
+ipv4.dhcp.expiry                | string    | ipv4 dhcp             | 1h                        | When to expire DHCP leases
 ipv4.dhcp.ranges                | string    | ipv4 dhcp             | all addresses             | Comma separated list of IP ranges to use for DHCP (FIRST-LAST format)
 ipv4.firewall                   | boolean   | ipv4 address          | true                      | Whether to generate filtering firewall rules for this network
 ipv4.routes                     | string    | ipv4 address          | -                         | Comma separated list of additional IPv4 CIDR subnets to route to the bridge
@@ -362,6 +363,7 @@ ipv4.routing                    | boolean   | ipv4 address          | true
 ipv6.address                    | string    | standard mode         | random unused subnet      | IPv6 address for the bridge (CIDR notation). Use "none" to turn off IPv6 or "auto" to generate a new one
 ipv6.nat                        | boolean   | ipv6 address          | false                     | Whether to NAT (will default to true if unset and a random ipv6.address is generated)
 ipv6.dhcp                       | boolean   | ipv6 address          | true                      | Whether to provide additional network configuration over DHCP
+ipv6.dhcp.expiry                | string    | ipv6 dhcp             | 1h                        | When to expire DHCP leases
 ipv6.dhcp.stateful              | boolean   | ipv6 dhcp             | false                     | Whether to allocate addresses using DHCP
 ipv6.dhcp.ranges                | string    | ipv6 stateful dhcp    | all addresses             | Comma separated list of IPv6 ranges to use for DHCP (FIRST-LAST format)
 ipv6.firewall                   | boolean   | ipv6 address          | true                      | Whether to generate filtering firewall rules for this network
diff --git a/lxd/api_1.0.go b/lxd/api_1.0.go
index 215f4a9..77bbc3e 100644
--- a/lxd/api_1.0.go
+++ b/lxd/api_1.0.go
@@ -107,6 +107,7 @@ func api10Get(d *Daemon, r *http.Request) Response {
 			"storage",
 			"file_delete",
 			"file_append",
+			"network_dhcp_expiry",
 		},
 		APIStatus:  "stable",
 		APIVersion: version.APIVersion,
diff --git a/lxd/networks.go b/lxd/networks.go
index 63a298c..8a03d56 100644
--- a/lxd/networks.go
+++ b/lxd/networks.go
@@ -742,13 +742,18 @@ func (n *network) Start() error {
 				dnsmasqCmd = append(dnsmasqCmd, []string{"--dhcp-no-override", "--dhcp-authoritative", fmt.Sprintf("--dhcp-leasefile=%s", shared.VarPath("networks", n.name, "dnsmasq.leases")), fmt.Sprintf("--dhcp-hostsfile=%s", shared.VarPath("networks", n.name, "dnsmasq.hosts"))}...)
 			}
 
+			expiry := "1h"
+			if n.config["ipv4.dhcp.expiry"] != "" {
+				expiry = n.config["ipv4.dhcp.expiry"]
+			}
+
 			if n.config["ipv4.dhcp.ranges"] != "" {
 				for _, dhcpRange := range strings.Split(n.config["ipv4.dhcp.ranges"], ",") {
 					dhcpRange = strings.TrimSpace(dhcpRange)
-					dnsmasqCmd = append(dnsmasqCmd, []string{"--dhcp-range", strings.Replace(dhcpRange, "-", ",", -1)}...)
+					dnsmasqCmd = append(dnsmasqCmd, []string{"--dhcp-range", fmt.Sprintf("%s,%s", strings.Replace(dhcpRange, "-", ",", -1), expiry)}...)
 				}
 			} else {
-				dnsmasqCmd = append(dnsmasqCmd, []string{"--dhcp-range", fmt.Sprintf("%s,%s", networkGetIP(subnet, 2).String(), networkGetIP(subnet, -2).String())}...)
+				dnsmasqCmd = append(dnsmasqCmd, []string{"--dhcp-range", fmt.Sprintf("%s,%s,%s", networkGetIP(subnet, 2).String(), networkGetIP(subnet, -2).String(), expiry)}...)
 			}
 		}
 
@@ -821,14 +826,19 @@ func (n *network) Start() error {
 				dnsmasqCmd = append(dnsmasqCmd, []string{"--dhcp-no-override", "--dhcp-authoritative", fmt.Sprintf("--dhcp-leasefile=%s", shared.VarPath("networks", n.name, "dnsmasq.leases")), fmt.Sprintf("--dhcp-hostsfile=%s", shared.VarPath("networks", n.name, "dnsmasq.hosts"))}...)
 			}
 
+			expiry := "1h"
+			if n.config["ipv6.dhcp.expiry"] != "" {
+				expiry = n.config["ipv6.dhcp.expiry"]
+			}
+
 			if shared.IsTrue(n.config["ipv6.dhcp.stateful"]) {
 				if n.config["ipv6.dhcp.ranges"] != "" {
 					for _, dhcpRange := range strings.Split(n.config["ipv6.dhcp.ranges"], ",") {
 						dhcpRange = strings.TrimSpace(dhcpRange)
-						dnsmasqCmd = append(dnsmasqCmd, []string{"--dhcp-range", fmt.Sprintf("%s", strings.Replace(dhcpRange, "-", ",", -1))}...)
+						dnsmasqCmd = append(dnsmasqCmd, []string{"--dhcp-range", fmt.Sprintf("%s,%s", strings.Replace(dhcpRange, "-", ",", -1), expiry)}...)
 					}
 				} else {
-					dnsmasqCmd = append(dnsmasqCmd, []string{"--dhcp-range", fmt.Sprintf("%s,%s", networkGetIP(subnet, 2), networkGetIP(subnet, -1))}...)
+					dnsmasqCmd = append(dnsmasqCmd, []string{"--dhcp-range", fmt.Sprintf("%s,%s,%s", networkGetIP(subnet, 2), networkGetIP(subnet, -1), expiry)}...)
 				}
 			} else {
 				dnsmasqCmd = append(dnsmasqCmd, []string{"--dhcp-range", fmt.Sprintf("::,constructor:%s,ra-stateless,ra-names", n.name)}...)
diff --git a/lxd/networks_config.go b/lxd/networks_config.go
index 0270045..fd6b17f 100644
--- a/lxd/networks_config.go
+++ b/lxd/networks_config.go
@@ -62,6 +62,7 @@ var networkConfigKeys = map[string]func(value string) error{
 	"ipv4.firewall":    shared.IsBool,
 	"ipv4.nat":         shared.IsBool,
 	"ipv4.dhcp":        shared.IsBool,
+	"ipv4.dhcp.expiry": shared.IsAny,
 	"ipv4.dhcp.ranges": shared.IsAny,
 	"ipv4.routes":      shared.IsAny,
 	"ipv4.routing":     shared.IsBool,
@@ -76,6 +77,7 @@ var networkConfigKeys = map[string]func(value string) error{
 	"ipv6.firewall":      shared.IsBool,
 	"ipv6.nat":           shared.IsBool,
 	"ipv6.dhcp":          shared.IsBool,
+	"ipv6.dhcp.expiry":   shared.IsAny,
 	"ipv6.dhcp.stateful": shared.IsBool,
 	"ipv6.dhcp.ranges":   shared.IsAny,
 	"ipv6.routes":        shared.IsAny,


More information about the lxc-devel mailing list