[lxc-devel] [lxd/master] Last beta2 changes

stgraber on Github lxc-bot at linuxcontainers.org
Wed Feb 10 21:31:35 UTC 2016


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/20160210/bd18d551/attachment.bin>
-------------- next part --------------
From a3f49ffaecb82e280134e517494cb74251a1e7b8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 10 Feb 2016 11:24:41 -0500
Subject: [PATCH 1/6] Remove backward compatibility code
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This has been around for a long time now and nobody should be using
those old versions anymore, so lets clean things up before we get to
2.0.

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 client.go       | 63 ++++++++++++++++++---------------------------------------
 lxc/copy.go     | 52 ++++++++---------------------------------------
 lxc/image.go    |  6 ++----
 lxd/images.go   |  5 +----
 lxd/migrate.go  | 15 ++------------
 shared/image.go | 30 ++++++++++-----------------
 shared/util.go  | 16 ---------------
 7 files changed, 44 insertions(+), 143 deletions(-)

diff --git a/client.go b/client.go
index 6bb9550..136b96c 100644
--- a/client.go
+++ b/client.go
@@ -530,8 +530,7 @@ func (c *Client) CopyImage(image string, dest *Client, copy_aliases bool, aliase
 		"server":      c.BaseURL,
 		"fingerprint": fingerprint}
 
-	// FIXME: InterfaceToBool is there for backward compatibility
-	if !shared.InterfaceToBool(info.Public) {
+	if !info.Public {
 		var secret string
 
 		resp, err := c.post("images/"+fingerprint+"/secret", nil, Async)
@@ -540,19 +539,13 @@ func (c *Client) CopyImage(image string, dest *Client, copy_aliases bool, aliase
 		}
 
 		op, err := resp.MetadataAsOperation()
-		if err == nil && op.Metadata != nil {
-			secret, err = op.Metadata.GetString("secret")
-			if err != nil {
-				return err
-			}
-		} else {
-			// FIXME: This is a backward compatibility codepath
-			md := secretMd{}
-			if err := json.Unmarshal(resp.Metadata, &md); err != nil {
-				return err
-			}
+		if err != nil {
+			return err
+		}
 
-			secret = md.Secret
+		secret, err = op.Metadata.GetString("secret")
+		if err != nil {
+			return err
 		}
 
 		source["secret"] = secret
@@ -1158,8 +1151,7 @@ func (c *Client) Init(name string, imgremote string, image string, profiles *[]s
 			return nil, fmt.Errorf("The image architecture is incompatible with the target server")
 		}
 
-		// FIXME: InterfaceToBool is there for backward compatibility
-		if !shared.InterfaceToBool(imageinfo.Public) {
+		if !imageinfo.Public {
 			var secret string
 
 			resp, err := tmpremote.post("images/"+fingerprint+"/secret", nil, Async)
@@ -1168,19 +1160,13 @@ func (c *Client) Init(name string, imgremote string, image string, profiles *[]s
 			}
 
 			op, err := resp.MetadataAsOperation()
-			if err == nil && op.Metadata != nil {
-				secret, err = op.Metadata.GetString("secret")
-				if err != nil {
-					return nil, err
-				}
-			} else {
-				// FIXME: This is a backward compatibility codepath
-				md := secretMd{}
-				if err := json.Unmarshal(resp.Metadata, &md); err != nil {
-					return nil, err
-				}
+			if err != nil {
+				return nil, err
+			}
 
-				secret = md.Secret
+			secret, err = op.Metadata.GetString("secret")
+			if err != nil {
+				return nil, err
 			}
 
 			source["secret"] = secret
@@ -1332,22 +1318,13 @@ func (c *Client) Exec(name string, cmd []string, env map[string]string,
 	var fds shared.Jmap
 
 	op, err := resp.MetadataAsOperation()
-	if err == nil && op.Metadata != nil {
-		fds, err = op.Metadata.GetMap("fds")
-		if err != nil {
-			return -1, err
-		}
-	} else {
-		// FIXME: This is a backward compatibility codepath
-		md := execMd{}
-		if err := json.Unmarshal(resp.Metadata, &md); err != nil {
-			return -1, err
-		}
+	if err != nil {
+		return -1, err
+	}
 
-		fds, err = shared.ParseMetadata(md.FDs)
-		if err != nil {
-			return -1, err
-		}
+	fds, err = op.Metadata.GetMap("fds")
+	if err != nil {
+		return -1, err
 	}
 
 	if controlHandler != nil {
diff --git a/lxc/copy.go b/lxc/copy.go
index a07480e..0f3d20d 100644
--- a/lxc/copy.go
+++ b/lxc/copy.go
@@ -1,7 +1,6 @@
 package main
 
 import (
-	"encoding/json"
 	"fmt"
 	"strings"
 
@@ -122,15 +121,12 @@ func copyContainer(config *lxd.Config, sourceResource string, destResource strin
 		secrets := map[string]string{}
 
 		op, err := sourceWSResponse.MetadataAsOperation()
-		if err == nil && op.Metadata != nil {
-			for k, v := range *op.Metadata {
-				secrets[k] = v.(string)
-			}
-		} else {
-			// FIXME: This is a backward compatibility codepath
-			if err := json.Unmarshal(sourceWSResponse.Metadata, &secrets); err != nil {
-				return err
-			}
+		if err != nil {
+			return err
+		}
+
+		for k, v := range *op.Metadata {
+			secrets[k] = v.(string)
 		}
 
 		addresses, err := source.Addresses()
@@ -146,55 +142,23 @@ func copyContainer(config *lxd.Config, sourceResource string, destResource strin
 		 * course, if all the errors are websocket errors, let's just
 		 * report that.
 		 */
-		var realError error
-
 		for _, addr := range addresses {
 			var migration *lxd.Response
 
 			sourceWSUrl := "https://" + addr + sourceWSResponse.Operation
 			migration, err = dest.MigrateFrom(destName, sourceWSUrl, secrets, status.Architecture, status.Config, status.Devices, status.Profiles, baseImage, ephemeral == 1)
 			if err != nil {
-				if !strings.Contains(err.Error(), "websocket: bad handshake") {
-					realError = err
-				}
-				shared.Debugf("intermediate error: %s", err)
 				continue
 			}
 
 			if err = dest.WaitForSuccess(migration.Operation); err != nil {
-				if !strings.Contains(err.Error(), "websocket: bad handshake") {
-					realError = err
-				}
-				shared.Debugf("intermediate error: %s", err)
-				// FIXME: This is a backward compatibility codepath
-				sourceWSUrl := "wss://" + addr + sourceWSResponse.Operation + "/websocket"
-
-				migration, err = dest.MigrateFrom(destName, sourceWSUrl, secrets, status.Architecture, status.Config, status.Devices, status.Profiles, baseImage, ephemeral == 1)
-				if err != nil {
-					if !strings.Contains(err.Error(), "websocket: bad handshake") {
-						realError = err
-					}
-					shared.Debugf("intermediate error: %s", err)
-					continue
-				}
-
-				if err = dest.WaitForSuccess(migration.Operation); err != nil {
-					if !strings.Contains(err.Error(), "websocket: bad handshake") {
-						realError = err
-					}
-					shared.Debugf("intermediate error: %s", err)
-					continue
-				}
+				return err
 			}
 
 			return nil
 		}
 
-		if realError != nil {
-			return realError
-		} else {
-			return err
-		}
+		return err
 	}
 }
 
diff --git a/lxc/image.go b/lxc/image.go
index 9ace054..2be6fe9 100644
--- a/lxc/image.go
+++ b/lxc/image.go
@@ -255,8 +255,7 @@ func (c *imageCmd) run(config *lxd.Config, args []string) error {
 		fmt.Printf(i18n.G("Fingerprint: %s")+"\n", info.Fingerprint)
 		public := i18n.G("no")
 
-		// FIXME: InterfaceToBool is there for backward compatibility
-		if shared.InterfaceToBool(info) {
+		if info.Public {
 			public = i18n.G("yes")
 		}
 
@@ -504,8 +503,7 @@ func showImages(images []shared.ImageInfo, filters []string) error {
 		public := i18n.G("no")
 		description := findDescription(image.Properties)
 
-		// FIXME: InterfaceToBool is there for backward compatibility
-		if shared.InterfaceToBool(image.Public) {
+		if image.Public {
 			public = i18n.G("yes")
 		}
 
diff --git a/lxd/images.go b/lxd/images.go
index c69f8b9..a24de23 100644
--- a/lxd/images.go
+++ b/lxd/images.go
@@ -660,10 +660,7 @@ func imageBuildFromInfo(d *Daemon, info shared.ImageInfo) (metadata map[string]s
 		info.Fingerprint,
 		info.Filename,
 		info.Size,
-
-		// FIXME: InterfaceToBool is there for backward compatibility
-		shared.InterfaceToBool(info.Public),
-
+		info.Public,
 		info.Architecture,
 		info.CreationDate,
 		info.ExpiryDate,
diff --git a/lxd/migrate.go b/lxd/migrate.go
index 46f2efd..254ad4d 100644
--- a/lxd/migrate.go
+++ b/lxd/migrate.go
@@ -446,20 +446,9 @@ func (c *migrationSink) connectWithSecret(secret string) (*websocket.Conn, error
 	query := url.Values{"secret": []string{secret}}
 
 	// The URL is a https URL to the operation, mangle to be a wss URL to the secret
-	url := c.url
-	if strings.HasPrefix(url, "https://") {
-		url = fmt.Sprintf("wss://%s", strings.TrimPrefix(url, "https://"))
-	}
-
-	// FIXME: This is a backward compatibility codepath
-	if !strings.HasSuffix(url, "/websocket") {
-		url = fmt.Sprintf("%s/websocket", url)
-	}
-
-	// Append the secret suffix
-	url = fmt.Sprintf("%s?%s", url, query.Encode())
+	wsUrl := fmt.Sprintf("wss://%s/websocket?%s", strings.TrimPrefix(c.url, "https://"), query.Encode())
 
-	return lxd.WebsocketDial(c.dialer, url)
+	return lxd.WebsocketDial(c.dialer, wsUrl)
 }
 
 func (c *migrationSink) do() error {
diff --git a/shared/image.go b/shared/image.go
index 692ed3c..c3d3073 100644
--- a/shared/image.go
+++ b/shared/image.go
@@ -15,14 +15,11 @@ type ImageInfo struct {
 	Fingerprint  string            `json:"fingerprint"`
 	Filename     string            `json:"filename"`
 	Properties   map[string]string `json:"properties"`
-
-	// FIXME: This is an interface{] instead of a bool for backward compatibility
-	Public interface{} `json:"public"`
-
-	Size         int64 `json:"size"`
-	CreationDate int64 `json:"created_at"`
-	ExpiryDate   int64 `json:"expires_at"`
-	UploadDate   int64 `json:"uploaded_at"`
+	Public       bool              `json:"public"`
+	Size         int64             `json:"size"`
+	CreationDate int64             `json:"created_at"`
+	ExpiryDate   int64             `json:"expires_at"`
+	UploadDate   int64             `json:"uploaded_at"`
 }
 
 /*
@@ -37,21 +34,16 @@ type BriefImageInfo struct {
 func (i *ImageInfo) BriefInfo() BriefImageInfo {
 	retstate := BriefImageInfo{
 		Properties: i.Properties,
-
-		// FIXME: InterfaceToBool is there for backward compatibility
-		Public: InterfaceToBool(i.Public)}
+		Public:     i.Public}
 	return retstate
 }
 
 type ImageBaseInfo struct {
-	Id          int
-	Fingerprint string
-	Filename    string
-	Size        int64
-
-	// FIXME: This is an interface{] instead of a bool for backward compatibility
-	Public interface{}
-
+	Id           int
+	Fingerprint  string
+	Filename     string
+	Size         int64
+	Public       bool
 	Architecture int
 	CreationDate int64
 	ExpiryDate   int64
diff --git a/shared/util.go b/shared/util.go
index 3615789..c3c6a29 100644
--- a/shared/util.go
+++ b/shared/util.go
@@ -501,22 +501,6 @@ func ValidHostname(name string) bool {
 	return true
 }
 
-// FIXME: InterfaceToBool is there for backward compatibility
-func InterfaceToBool(value interface{}) bool {
-	switch t := value.(type) {
-	case bool:
-		return t
-	case float32:
-		return t == 1
-	case float64:
-		return t == 1
-	case int:
-		return t == 1
-	default:
-		return false
-	}
-}
-
 func TextEditor(inPath string, inContent []byte) ([]byte, error) {
 	var f *os.File
 	var err error

From 6cec2158e513dade1af838be3ced08b0e8a1da6e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 10 Feb 2016 14:08:33 -0500
Subject: [PATCH 2/6] Fail when unsetting a key that's not currentl set
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Closes #1477

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxc/config.go | 40 ++++++++++++++++++++++++---
 po/lxd.pot    | 86 +++++++++++++++++++++++++++++++++--------------------------
 2 files changed, 84 insertions(+), 42 deletions(-)

diff --git a/lxc/config.go b/lxc/config.go
index 19709bb..1f95535 100644
--- a/lxc/config.go
+++ b/lxc/config.go
@@ -90,7 +90,7 @@ To set the server trust password:
     lxc config set core.trust_password blah`)
 }
 
-func doSet(config *lxd.Config, args []string) error {
+func doSet(config *lxd.Config, args []string, unset bool) error {
 	if len(args) != 4 {
 		return errArgs
 	}
@@ -108,11 +108,23 @@ func doSet(config *lxd.Config, args []string) error {
 	if !terminal.IsTerminal(int(syscall.Stdin)) && value == "-" {
 		buf, err := ioutil.ReadAll(os.Stdin)
 		if err != nil {
-			return fmt.Errorf("Can't read from stdin: %s", err)
+			return fmt.Errorf(i18n.G("Can't read from stdin: %s"), err)
 		}
 		value = string(buf[:])
 	}
 
+	if unset {
+		st, err := d.ContainerStatus(container)
+		if err != nil {
+			return err
+		}
+
+		_, ok := st.Config[key]
+		if !ok {
+			return fmt.Errorf(i18n.G("Can't unset key '%s', it's not currently set."), key)
+		}
+	}
+
 	return d.SetContainerConfig(container, key, value)
 }
 
@@ -135,6 +147,16 @@ func (c *configCmd) run(config *lxd.Config, args []string) error {
 				return err
 			}
 
+			ss, err := c.ServerStatus()
+			if err != nil {
+				return err
+			}
+
+			_, ok := ss.Config[args[1]]
+			if !ok {
+				return fmt.Errorf(i18n.G("Can't unset key '%s', it's not currently set."), args[1])
+			}
+
 			_, err = c.SetServerConfig(args[1], "")
 			return err
 		}
@@ -147,13 +169,23 @@ func (c *configCmd) run(config *lxd.Config, args []string) error {
 				return err
 			}
 
+			ss, err := c.ServerStatus()
+			if err != nil {
+				return err
+			}
+
+			_, ok := ss.Config[args[1]]
+			if !ok {
+				return fmt.Errorf(i18n.G("Can't unset key '%s', it's not currently set."), args[1])
+			}
+
 			_, err = c.SetServerConfig(args[2], "")
 			return err
 		}
 
 		// Deal with container
 		args = append(args, "")
-		return doSet(config, args)
+		return doSet(config, args, true)
 
 	case "set":
 		if len(args) < 3 {
@@ -184,7 +216,7 @@ func (c *configCmd) run(config *lxd.Config, args []string) error {
 		}
 
 		// Deal with container
-		return doSet(config, args)
+		return doSet(config, args, false)
 
 	case "trust":
 		if len(args) < 2 {
diff --git a/po/lxd.pot b/po/lxd.pot
index 5baf553..f423d71 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: 2016-02-09 15:53-0700\n"
+        "POT-Creation-Date: 2016-02-10 14:08-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"
@@ -65,7 +65,7 @@ msgid   "### This is a yaml representation of the profile.\n"
         "### Note that the name is shown but cannot be changed"
 msgstr  ""
 
-#: lxc/image.go:501
+#: lxc/image.go:500
 #, c-format
 msgid   "%s (%d more)"
 msgstr  ""
@@ -78,11 +78,11 @@ msgstr  ""
 msgid   "(none)"
 msgstr  ""
 
-#: lxc/image.go:522 lxc/image.go:544
+#: lxc/image.go:520 lxc/image.go:542
 msgid   "ALIAS"
 msgstr  ""
 
-#: lxc/image.go:526
+#: lxc/image.go:524
 msgid   "ARCH"
 msgstr  ""
 
@@ -95,7 +95,7 @@ msgstr  ""
 msgid   "Admin password for %s: "
 msgstr  ""
 
-#: lxc/image.go:282
+#: lxc/image.go:281
 msgid   "Aliases:"
 msgstr  ""
 
@@ -103,7 +103,7 @@ msgstr  ""
 msgid   "An environment variable of the form HOME=/home/foo"
 msgstr  ""
 
-#: lxc/image.go:265
+#: lxc/image.go:264
 #, c-format
 msgid   "Architecture: %s"
 msgstr  ""
@@ -112,10 +112,20 @@ msgstr  ""
 msgid   "Available commands:"
 msgstr  ""
 
-#: lxc/config.go:232
+#: lxc/config.go:264
 msgid   "COMMON NAME"
 msgstr  ""
 
+#: lxc/config.go:111
+#, c-format
+msgid   "Can't read from stdin: %s"
+msgstr  ""
+
+#: lxc/config.go:124 lxc/config.go:157 lxc/config.go:179
+#, c-format
+msgid   "Can't unset key '%s', it's not currently set."
+msgstr  ""
+
 #: lxc/profile.go:329
 msgid   "Cannot provide container name to list"
 msgstr  ""
@@ -144,7 +154,7 @@ msgstr  ""
 msgid   "Config key/value to apply to the new container"
 msgstr  ""
 
-#: lxc/config.go:458 lxc/config.go:523 lxc/image.go:599 lxc/profile.go:185
+#: lxc/config.go:490 lxc/config.go:555 lxc/image.go:597 lxc/profile.go:185
 #, c-format
 msgid   "Config parsing error: %s"
 msgstr  ""
@@ -171,7 +181,7 @@ msgstr  ""
 msgid   "Copy aliases from source"
 msgstr  ""
 
-#: lxc/copy.go:23
+#: lxc/copy.go:22
 msgid   "Copy containers within or in between lxd instances.\n"
         "\n"
         "lxc copy [remote:]<source container> [remote:]<destination container> [--ephemeral|e]"
@@ -198,7 +208,7 @@ msgid   "Create a read-only snapshot of a container.\n"
         "lxc snapshot u1 snap0"
 msgstr  ""
 
-#: lxc/image.go:270
+#: lxc/image.go:269
 #, c-format
 msgid   "Created: %s"
 msgstr  ""
@@ -212,7 +222,7 @@ msgstr  ""
 msgid   "Creating the container"
 msgstr  ""
 
-#: lxc/image.go:525
+#: lxc/image.go:523
 msgid   "DESCRIPTION"
 msgstr  ""
 
@@ -224,12 +234,12 @@ msgid   "Delete containers or container snapshots.\n"
         "Destroy containers or snapshots with any attached data (configuration, snapshots, ...)."
 msgstr  ""
 
-#: lxc/config.go:571
+#: lxc/config.go:603
 #, c-format
 msgid   "Device %s added to %s"
 msgstr  ""
 
-#: lxc/config.go:599
+#: lxc/config.go:631
 #, c-format
 msgid   "Device %s removed from %s"
 msgstr  ""
@@ -238,7 +248,7 @@ msgstr  ""
 msgid   "EPHEMERAL"
 msgstr  ""
 
-#: lxc/config.go:234
+#: lxc/config.go:266
 msgid   "EXPIRY DATE"
 msgstr  ""
 
@@ -254,7 +264,7 @@ msgstr  ""
 msgid   "Environment:"
 msgstr  ""
 
-#: lxc/copy.go:30 lxc/copy.go:31 lxc/init.go:136 lxc/init.go:137 lxc/launch.go:40 lxc/launch.go:41
+#: lxc/copy.go:29 lxc/copy.go:30 lxc/init.go:136 lxc/init.go:137 lxc/launch.go:40 lxc/launch.go:41
 msgid   "Ephemeral container"
 msgstr  ""
 
@@ -268,16 +278,16 @@ msgid   "Execute the specified command in a container.\n"
         "lxc exec [remote:]container [--mode=auto|interactive|non-interactive] [--env EDITOR=/usr/bin/vim]... <command>"
 msgstr  ""
 
-#: lxc/image.go:274
+#: lxc/image.go:273
 #, c-format
 msgid   "Expires: %s"
 msgstr  ""
 
-#: lxc/image.go:276
+#: lxc/image.go:275
 msgid   "Expires: never"
 msgstr  ""
 
-#: lxc/config.go:231 lxc/image.go:523 lxc/image.go:545
+#: lxc/config.go:263 lxc/image.go:521 lxc/image.go:543
 msgid   "FINGERPRINT"
 msgstr  ""
 
@@ -316,7 +326,7 @@ msgstr  ""
 msgid   "IPV6"
 msgstr  ""
 
-#: lxc/config.go:233
+#: lxc/config.go:265
 msgid   "ISSUE DATE"
 msgstr  ""
 
@@ -328,7 +338,7 @@ msgstr  ""
 msgid   "Image copied successfully!"
 msgstr  ""
 
-#: lxc/image.go:340
+#: lxc/image.go:339
 #, c-format
 msgid   "Image imported with fingerprint: %s"
 msgstr  ""
@@ -627,15 +637,15 @@ msgstr  ""
 msgid   "New alias to define at target"
 msgstr  ""
 
-#: lxc/config.go:245
+#: lxc/config.go:277
 msgid   "No certificate provided to add"
 msgstr  ""
 
-#: lxc/config.go:268
+#: lxc/config.go:300
 msgid   "No fingerprint specified."
 msgstr  ""
 
-#: lxc/image.go:332
+#: lxc/image.go:331
 msgid   "Only https:// is supported for remote image import."
 msgstr  ""
 
@@ -643,7 +653,7 @@ msgstr  ""
 msgid   "Options:"
 msgstr  ""
 
-#: lxc/image.go:426
+#: lxc/image.go:425
 #, c-format
 msgid   "Output is in %s"
 msgstr  ""
@@ -656,7 +666,7 @@ msgstr  ""
 msgid   "PID"
 msgstr  ""
 
-#: lxc/image.go:524 lxc/remote.go:273
+#: lxc/image.go:522 lxc/remote.go:273
 msgid   "PUBLIC"
 msgstr  ""
 
@@ -682,7 +692,7 @@ msgstr  ""
 msgid   "Press enter to open the editor again"
 msgstr  ""
 
-#: lxc/config.go:459 lxc/config.go:524 lxc/image.go:600
+#: lxc/config.go:491 lxc/config.go:556 lxc/image.go:598
 msgid   "Press enter to start the editor again"
 msgstr  ""
 
@@ -733,7 +743,7 @@ msgstr  ""
 msgid   "Profiles: %s"
 msgstr  ""
 
-#: lxc/image.go:278
+#: lxc/image.go:277
 msgid   "Properties:"
 msgstr  ""
 
@@ -741,7 +751,7 @@ msgstr  ""
 msgid   "Public image server"
 msgstr  ""
 
-#: lxc/image.go:266
+#: lxc/image.go:265
 #, c-format
 msgid   "Public: %s"
 msgstr  ""
@@ -761,7 +771,7 @@ msgstr  ""
 msgid   "Retrieving image: %s"
 msgstr  ""
 
-#: lxc/image.go:527
+#: lxc/image.go:525
 msgid   "SIZE"
 msgstr  ""
 
@@ -814,7 +824,7 @@ msgstr  ""
 msgid   "Show the container's last 100 log lines?"
 msgstr  ""
 
-#: lxc/image.go:263
+#: lxc/image.go:262
 #, c-format
 msgid   "Size: %.2fMB"
 msgstr  ""
@@ -845,7 +855,7 @@ msgstr  ""
 msgid   "Time to wait for the container before killing it."
 msgstr  ""
 
-#: lxc/image.go:267
+#: lxc/image.go:266
 msgid   "Timestamps:"
 msgstr  ""
 
@@ -862,7 +872,7 @@ msgstr  ""
 msgid   "Type: persistent"
 msgstr  ""
 
-#: lxc/image.go:528
+#: lxc/image.go:526
 msgid   "UPLOAD DATE"
 msgstr  ""
 
@@ -870,7 +880,7 @@ msgstr  ""
 msgid   "URL"
 msgstr  ""
 
-#: lxc/image.go:272
+#: lxc/image.go:271
 #, c-format
 msgid   "Uploaded: %s"
 msgstr  ""
@@ -912,7 +922,7 @@ msgstr  ""
 msgid   "bad result type from action"
 msgstr  ""
 
-#: lxc/copy.go:79
+#: lxc/copy.go:78
 msgid   "can't copy to the same container name"
 msgstr  ""
 
@@ -942,11 +952,11 @@ msgstr  ""
 msgid   "got bad version"
 msgstr  ""
 
-#: lxc/image.go:256 lxc/image.go:504
+#: lxc/image.go:256 lxc/image.go:503
 msgid   "no"
 msgstr  ""
 
-#: lxc/copy.go:101
+#: lxc/copy.go:100
 msgid   "not all the profiles from the source exist on the target"
 msgstr  ""
 
@@ -982,11 +992,11 @@ msgstr  ""
 msgid   "wrong number of subcommand arguments"
 msgstr  ""
 
-#: lxc/image.go:260 lxc/image.go:509
+#: lxc/image.go:259 lxc/image.go:507
 msgid   "yes"
 msgstr  ""
 
-#: lxc/copy.go:39
+#: lxc/copy.go:38
 msgid   "you must specify a source container name"
 msgstr  ""
 

From 7959797f3fd9698bc4552b8183c47465ff112f9e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 10 Feb 2016 14:40:32 -0500
Subject: [PATCH 3/6] Implement interactive mode in delete
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Closes #781

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxc/delete.go            | 44 ++++++++++++++++++++++++++++++++++++--------
 po/lxd.pot               | 29 +++++++++++++++++++++++++----
 test/main.sh             |  2 +-
 test/suites/basic.sh     |  4 ++--
 test/suites/filemanip.sh |  2 +-
 5 files changed, 65 insertions(+), 16 deletions(-)

diff --git a/lxc/delete.go b/lxc/delete.go
index d241aea..d09498d 100644
--- a/lxc/delete.go
+++ b/lxc/delete.go
@@ -1,14 +1,21 @@
 package main
 
 import (
+	"bufio"
 	"fmt"
+	"os"
+	"strings"
 
 	"github.com/lxc/lxd"
 	"github.com/lxc/lxd/i18n"
 	"github.com/lxc/lxd/shared"
+	"github.com/lxc/lxd/shared/gnuflag"
 )
 
-type deleteCmd struct{}
+type deleteCmd struct {
+	force       bool
+	interactive bool
+}
 
 func (c *deleteCmd) showByDefault() bool {
 	return true
@@ -23,9 +30,24 @@ lxc delete [remote:]<container>[/<snapshot>] [remote:][<container>[/<snapshot>].
 Destroy containers or snapshots with any attached data (configuration, snapshots, ...).`)
 }
 
-func (c *deleteCmd) flags() {}
+func (c *deleteCmd) flags() {
+	gnuflag.BoolVar(&c.force, "f", false, i18n.G("Force the removal of stopped containers."))
+	gnuflag.BoolVar(&c.force, "force", false, i18n.G("Force the removal of stopped containers."))
+	gnuflag.BoolVar(&c.interactive, "i", false, i18n.G("Require user confirmation."))
+	gnuflag.BoolVar(&c.interactive, "interactive", false, i18n.G("Require user confirmation."))
+}
+
+func (c *deleteCmd) doDelete(d *lxd.Client, name string) error {
+	if c.interactive {
+		reader := bufio.NewReader(os.Stdin)
+		fmt.Printf(i18n.G("Remove %s (yes/no): "), name)
+		input, _ := reader.ReadString('\n')
+		input = strings.TrimSuffix(input, "\n")
+		if !shared.StringInSlice(strings.ToLower(input), []string{i18n.G("yes")}) {
+			return fmt.Errorf(i18n.G("User aborted delete operation."))
+		}
+	}
 
-func doDelete(d *lxd.Client, name string) error {
 	resp, err := d.Delete(name)
 	if err != nil {
 		return err
@@ -47,14 +69,20 @@ func (c *deleteCmd) run(config *lxd.Config, args []string) error {
 			return err
 		}
 
-		ct, err := d.ContainerStatus(name)
+		if shared.IsSnapshot(name) {
+			return c.doDelete(d, name)
+		}
 
+		ct, err := d.ContainerStatus(name)
 		if err != nil {
-			// Could be a snapshot
-			return doDelete(d, name)
+			return err
 		}
 
 		if ct.Status.StatusCode != 0 && ct.Status.StatusCode != shared.Stopped {
+			if !c.force {
+				return fmt.Errorf(i18n.G("The container is currently running, stop it first or pass --force."))
+			}
+
 			resp, err := d.Action(name, shared.Stop, -1, true)
 			if err != nil {
 				return err
@@ -73,10 +101,10 @@ func (c *deleteCmd) run(config *lxd.Config, args []string) error {
 				return nil
 			}
 		}
-		if err := doDelete(d, name); err != nil {
+
+		if err := c.doDelete(d, name); err != nil {
 			return err
 		}
 	}
 	return nil
-
 }
diff --git a/po/lxd.pot b/po/lxd.pot
index f423d71..2afd38d 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: 2016-02-10 14:08-0500\n"
+        "POT-Creation-Date: 2016-02-10 14:55-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"
@@ -226,7 +226,7 @@ msgstr  ""
 msgid   "DESCRIPTION"
 msgstr  ""
 
-#: lxc/delete.go:18
+#: lxc/delete.go:25
 msgid   "Delete containers or container snapshots.\n"
         "\n"
         "lxc delete [remote:]<container>[/<snapshot>] [remote:][<container>[/<snapshot>]...]\n"
@@ -310,6 +310,10 @@ msgstr  ""
 msgid   "Force the container to shutdown."
 msgstr  ""
 
+#: lxc/delete.go:34 lxc/delete.go:35
+msgid   "Force the removal of stopped containers."
+msgstr  ""
+
 #: lxc/main.go:56
 msgid   "Force using the local unix socket."
 msgstr  ""
@@ -766,6 +770,15 @@ msgstr  ""
 msgid   "Remote admin password"
 msgstr  ""
 
+#: lxc/delete.go:43
+#, c-format
+msgid   "Remove %s (yes/no): "
+msgstr  ""
+
+#: lxc/delete.go:36 lxc/delete.go:37
+msgid   "Require user confirmation."
+msgstr  ""
+
 #: lxc/init.go:244
 #, c-format
 msgid   "Retrieving image: %s"
@@ -843,10 +856,14 @@ msgstr  ""
 msgid   "Status: %s"
 msgstr  ""
 
-#: lxc/delete.go:69
+#: lxc/delete.go:97
 msgid   "Stopping container failed!"
 msgstr  ""
 
+#: lxc/delete.go:83
+msgid   "The container is currently running, stop it first or pass --force."
+msgstr  ""
+
 #: lxc/publish.go:57
 msgid   "There is no \"image name\".  Did you want an alias?"
 msgstr  ""
@@ -894,6 +911,10 @@ msgstr  ""
 msgid   "Usage: lxc [subcommand] [options]"
 msgstr  ""
 
+#: lxc/delete.go:47
+msgid   "User aborted delete operation."
+msgstr  ""
+
 #: lxc/restore.go:35
 msgid   "Whether or not to restore the container's running state from snapshot (if available)"
 msgstr  ""
@@ -992,7 +1013,7 @@ msgstr  ""
 msgid   "wrong number of subcommand arguments"
 msgstr  ""
 
-#: lxc/image.go:259 lxc/image.go:507
+#: lxc/delete.go:46 lxc/image.go:259 lxc/image.go:507
 msgid   "yes"
 msgstr  ""
 
diff --git a/test/main.sh b/test/main.sh
index 55e05d6..e13bd2b 100755
--- a/test/main.sh
+++ b/test/main.sh
@@ -183,7 +183,7 @@ kill_lxd() {
     # Delete all containers
     echo "==> Deleting all containers"
     for container in $(lxc list --force-local | tail -n+3 | grep "^| " | cut -d' ' -f2); do
-      lxc delete "${container}" --force-local || true
+      lxc delete "${container}" --force-local -f || true
     done
 
     # Delete all images
diff --git a/test/suites/basic.sh b/test/suites/basic.sh
index 6b55f13..0a1e31a 100644
--- a/test/suites/basic.sh
+++ b/test/suites/basic.sh
@@ -211,10 +211,10 @@ test_basic_usage() {
 
   lxc launch testimage deleterunning
   my_curl -X DELETE "https://${LXD_ADDR}/1.0/containers/deleterunning" | grep "container is running"
-  lxc delete deleterunning
+  lxc delete deleterunning -f
 
   # cleanup
-  lxc delete foo
+  lxc delete foo -f
 
   # check that an apparmor profile is created for this container, that it is
   # unloaded on stop, and that it is deleted when the container is deleted
diff --git a/test/suites/filemanip.sh b/test/suites/filemanip.sh
index a8c2cb8..dd2317c 100644
--- a/test/suites/filemanip.sh
+++ b/test/suites/filemanip.sh
@@ -10,5 +10,5 @@ test_filemanip() {
   [ ! -f /tmp/main.sh ]
   [ -f "${LXD_DIR}/containers/filemanip/rootfs/tmp/main.sh" ]
 
-  lxc delete filemanip
+  lxc delete filemanip -f
 }

From 21d3b9b3dfe4c98ecff990fd2fe7e29d36404bb0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 10 Feb 2016 15:15:10 -0500
Subject: [PATCH 4/6] specs: Re-sync database spec with reality
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>
---
 specs/database.md | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/specs/database.md b/specs/database.md
index 6f5162d..2632fd5 100644
--- a/specs/database.md
+++ b/specs/database.md
@@ -109,8 +109,8 @@ id              | INTEGER       | SERIAL        | NOT NULL          | SERIAL
 name            | VARCHAR(255)  | -             | NOT NULL          | Container name
 architecture    | INTEGER       | -             | NOT NULL          | Container architecture
 type            | INTEGER       | 0             | NOT NULL          | Container type (0 = container, 1 = container snapshot)
-power\_state    | INTEGER       | 0             | NOT NULL          | Container power state (0 = off, 1 = on)
 ephemeral       | INTEGER       | 0             | NOT NULL          | Whether the container is ephemeral (0 = persistent, 1 = ephemeral)
+creation\_date  | DATETIME      | -             |                   | Image creation date (user supplied, 0 = unknown)
 
 Index: UNIQUE ON id AND name
 
@@ -190,31 +190,31 @@ last\_use\_date | DATETIME      | -             |                   | Last time
 Index: UNIQUE ON id AND fingerprint
 
 
-## images\_properties
+## images\_aliases
 
 Column          | Type          | Default       | Constraint        | Description
 :-----          | :---          | :------       | :---------        | :----------
 id              | INTEGER       | SERIAL        | NOT NULL          | SERIAL
+name            | VARCHAR(255)  | -             | NOT NULL          | Alias name
 image\_id       | INTEGER       | -             | NOT NULL          | images.id FK
-type            | INTEGER       | 0             | NOT NULL          | Property type (0 = string, 1 = text)
-key             | VARCHAR(255)  | -             | NOT NULL          | Property name
-value           | TEXT          | -             |                   | Property value (NULL for unset)
+description     | VARCHAR(255)  | -             |                   | Description of the alias
 
-Index: UNIQUE ON id
+Index: UNIQUE ON id AND name
 
 Foreign keys: image\_id REFERENCES images(id)
 
 
-## images\_aliases
+## images\_properties
 
 Column          | Type          | Default       | Constraint        | Description
 :-----          | :---          | :------       | :---------        | :----------
 id              | INTEGER       | SERIAL        | NOT NULL          | SERIAL
-name            | VARCHAR(255)  | -             | NOT NULL          | Alias name
 image\_id       | INTEGER       | -             | NOT NULL          | images.id FK
-description     | VARCHAR(255)  | -             |                   | Description of the alias
+type            | INTEGER       | 0             | NOT NULL          | Property type (0 = string, 1 = text)
+key             | VARCHAR(255)  | -             | NOT NULL          | Property name
+value           | TEXT          | -             |                   | Property value (NULL for unset)
 
-Index: UNIQUE ON id AND name
+Index: UNIQUE ON id
 
 Foreign keys: image\_id REFERENCES images(id)
 

From e63445b6f9acbd14d4aab29278724b1c0401e388 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 10 Feb 2016 16:00:59 -0500
Subject: [PATCH 5/6] Record a creation time for containers and snapshots
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.go          |  2 ++
 lxd/container_lxc.go      |  7 +++++++
 lxd/container_snapshot.go | 27 ++++++++++++---------------
 lxd/db.go                 |  3 ++-
 lxd/db_containers.go      | 12 ++++++++----
 lxd/db_images.go          |  4 ++--
 lxd/db_update.go          | 14 ++++++++++++++
 shared/container.go       |  7 +++++++
 specs/rest-api.md         |  3 +++
 test/suites/basic.sh      |  2 +-
 10 files changed, 58 insertions(+), 23 deletions(-)

diff --git a/lxd/container.go b/lxd/container.go
index 87dd19d..272b7f2 100644
--- a/lxd/container.go
+++ b/lxd/container.go
@@ -296,6 +296,7 @@ type containerArgs struct {
 	Architecture int
 	BaseImage    string
 	Config       map[string]string
+	CreationDate *time.Time
 	Ctype        containerType
 	Devices      shared.Devices
 	Ephemeral    bool
@@ -350,6 +351,7 @@ type container interface {
 	Id() int
 	Name() string
 	Architecture() int
+	CreationDate() *time.Time
 	ExpandedConfig() map[string]string
 	ExpandedDevices() shared.Devices
 	LocalConfig() map[string]string
diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index 073b9b7..3a22f74 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -86,6 +86,7 @@ func containerLXCCreate(d *Daemon, args containerArgs) (container, error) {
 		ephemeral:    args.Ephemeral,
 		architecture: args.Architecture,
 		cType:        args.Ctype,
+		creationDate: args.CreationDate,
 		profiles:     args.Profiles,
 		localConfig:  args.Config,
 		localDevices: args.Devices}
@@ -181,6 +182,7 @@ func containerLXCLoad(d *Daemon, args containerArgs) (container, error) {
 		ephemeral:    args.Ephemeral,
 		architecture: args.Architecture,
 		cType:        args.Ctype,
+		creationDate: args.CreationDate,
 		profiles:     args.Profiles,
 		localConfig:  args.Config,
 		localDevices: args.Devices}
@@ -206,6 +208,7 @@ type containerLXC struct {
 	// Properties
 	architecture int
 	cType        containerType
+	creationDate *time.Time
 	ephemeral    bool
 	id           int
 	name         string
@@ -1362,6 +1365,7 @@ func (c *containerLXC) RenderState() (*shared.ContainerState, error) {
 	return &shared.ContainerState{
 		Architecture:    c.architecture,
 		Config:          c.localConfig,
+		CreationDate:    c.creationDate.Unix(),
 		Devices:         c.localDevices,
 		Ephemeral:       c.ephemeral,
 		ExpandedConfig:  c.expandedConfig,
@@ -3825,6 +3829,9 @@ func (c *containerLXC) Architecture() int {
 	return c.architecture
 }
 
+func (c *containerLXC) CreationDate() *time.Time {
+	return c.creationDate
+}
 func (c *containerLXC) ExpandedConfig() map[string]string {
 	return c.expandedConfig
 }
diff --git a/lxd/container_snapshot.go b/lxd/container_snapshot.go
index dfff877..063364e 100644
--- a/lxd/container_snapshot.go
+++ b/lxd/container_snapshot.go
@@ -10,8 +10,6 @@ import (
 	"github.com/gorilla/mux"
 
 	"github.com/lxc/lxd/shared"
-
-	log "gopkg.in/inconshreveable/log15.v2"
 )
 
 func containerSnapshotsGet(d *Daemon, r *http.Request) Response {
@@ -22,13 +20,12 @@ func containerSnapshotsGet(d *Daemon, r *http.Request) Response {
 	}
 
 	cname := mux.Vars(r)["name"]
-	// Makes sure the requested container exists.
-	_, err = containerLoadByName(d, cname)
+	c, err := containerLoadByName(d, cname)
 	if err != nil {
 		return SmartError(err)
 	}
 
-	results, err := dbContainerGetSnapshots(d.db, cname)
+	snaps, err := c.Snapshots()
 	if err != nil {
 		return SmartError(err)
 	}
@@ -36,19 +33,16 @@ func containerSnapshotsGet(d *Daemon, r *http.Request) Response {
 	resultString := []string{}
 	resultMap := []shared.Jmap{}
 
-	for _, name := range results {
-		sc, err := containerLoadByName(d, name)
-		if err != nil {
-			shared.Log.Error("Failed to load snapshot", log.Ctx{"snapshot": name})
-			continue
-		}
-
-		snapName := strings.SplitN(name, shared.SnapshotDelimiter, 2)[1]
+	for _, snap := range snaps {
+		snapName := strings.SplitN(snap.Name(), shared.SnapshotDelimiter, 2)[1]
 		if recursion == 0 {
 			url := fmt.Sprintf("/%s/containers/%s/snapshots/%s", shared.APIVersion, cname, snapName)
 			resultString = append(resultString, url)
 		} else {
-			body := shared.Jmap{"name": snapName, "stateful": shared.PathExists(sc.StatePath())}
+			body := shared.Jmap{
+				"name":          snapName,
+				"creation_date": snap.CreationDate().Unix(),
+				"stateful":      shared.PathExists(snap.StatePath())}
 			resultMap = append(resultMap, body)
 		}
 	}
@@ -189,7 +183,10 @@ func snapshotHandler(d *Daemon, r *http.Request) Response {
 }
 
 func snapshotGet(sc container, name string) Response {
-	body := shared.Jmap{"name": name, "stateful": shared.PathExists(sc.StatePath())}
+	body := shared.Jmap{
+		"name":          name,
+		"creation_date": sc.CreationDate().Unix(),
+		"stateful":      shared.PathExists(sc.StatePath())}
 	return SyncResponse(true, body)
 }
 
diff --git a/lxd/db.go b/lxd/db.go
index 9b5606c..646d076 100644
--- a/lxd/db.go
+++ b/lxd/db.go
@@ -34,7 +34,7 @@ type Profile struct {
 // Profiles will contain a list of all Profiles.
 type Profiles []Profile
 
-const DB_CURRENT_VERSION int = 21
+const DB_CURRENT_VERSION int = 22
 
 // CURRENT_SCHEMA contains the current SQLite SQL Schema.
 const CURRENT_SCHEMA string = `
@@ -58,6 +58,7 @@ CREATE TABLE IF NOT EXISTS containers (
     architecture INTEGER NOT NULL,
     type INTEGER NOT NULL,
     ephemeral INTEGER NOT NULL DEFAULT 0,
+    creation_date DATETIME,
     UNIQUE (name)
 );
 CREATE TABLE IF NOT EXISTS containers_config (
diff --git a/lxd/db_containers.go b/lxd/db_containers.go
index a1092e8..c8a1e53 100644
--- a/lxd/db_containers.go
+++ b/lxd/db_containers.go
@@ -3,6 +3,7 @@ package main
 import (
 	"database/sql"
 	"fmt"
+	"time"
 
 	"github.com/lxc/lxd/shared"
 
@@ -65,9 +66,9 @@ func dbContainerGet(db *sql.DB, name string) (containerArgs, error) {
 	args.Name = name
 
 	ephemInt := -1
-	q := "SELECT id, architecture, type, ephemeral FROM containers WHERE name=?"
+	q := "SELECT id, architecture, type, ephemeral, creation_date FROM containers WHERE name=?"
 	arg1 := []interface{}{name}
-	arg2 := []interface{}{&args.Id, &args.Architecture, &args.Ctype, &ephemInt}
+	arg2 := []interface{}{&args.Id, &args.Architecture, &args.Ctype, &ephemInt, &args.CreationDate}
 	err := dbQueryRowScan(db, q, arg1, arg2)
 	if err != nil {
 		return args, err
@@ -123,14 +124,17 @@ func dbContainerCreate(db *sql.DB, args containerArgs) (int, error) {
 		ephemInt = 1
 	}
 
-	str := fmt.Sprintf("INSERT INTO containers (name, architecture, type, ephemeral) VALUES (?, ?, ?, ?)")
+	now := time.Now().UTC()
+	args.CreationDate = &now
+
+	str := fmt.Sprintf("INSERT INTO containers (name, architecture, type, ephemeral, creation_date) VALUES (?, ?, ?, ?, ?)")
 	stmt, err := tx.Prepare(str)
 	if err != nil {
 		tx.Rollback()
 		return 0, err
 	}
 	defer stmt.Close()
-	result, err := stmt.Exec(args.Name, args.Architecture, args.Ctype, ephemInt)
+	result, err := stmt.Exec(args.Name, args.Architecture, args.Ctype, ephemInt, args.CreationDate.Unix())
 	if err != nil {
 		tx.Rollback()
 		return 0, err
diff --git a/lxd/db_images.go b/lxd/db_images.go
index 3f42e2b..2cd41c7 100644
--- a/lxd/db_images.go
+++ b/lxd/db_images.go
@@ -150,9 +150,9 @@ func dbImageSetPublic(db *sql.DB, id int, public bool) error {
 	return err
 }
 
-// Insert an alias into the database.
+// Insert an alias ento the database.
 func dbImageAliasAdd(db *sql.DB, name string, imageID int, desc string) error {
-	stmt := `INSERT into images_aliases (name, image_id, description) values (?, ?, ?)`
+	stmt := `INSERT INTO images_aliases (name, image_id, description) values (?, ?, ?)`
 	_, err := dbExec(db, stmt, name, imageID, desc)
 	return err
 }
diff --git a/lxd/db_update.go b/lxd/db_update.go
index 6a7a8fe..0e39f84 100644
--- a/lxd/db_update.go
+++ b/lxd/db_update.go
@@ -15,6 +15,14 @@ import (
 	log "gopkg.in/inconshreveable/log15.v2"
 )
 
+func dbUpdateFromV21(db *sql.DB) error {
+	stmt := `
+ALTER TABLE containers ADD COLUMN creation_date DATETIME NOT NULL DEFAULT 0;
+INSERT INTO schema (version, updated_at) VALUES (?, strftime("%s"));`
+	_, err := db.Exec(stmt, 22)
+	return err
+}
+
 func dbUpdateFromV20(d *Daemon) error {
 	cNames, err := dbContainersList(d.db, cTypeRegular)
 	if err != nil {
@@ -900,6 +908,12 @@ func dbUpdate(d *Daemon, prevVersion int) error {
 			return err
 		}
 	}
+	if prevVersion < 22 {
+		err = dbUpdateFromV21(db)
+		if err != nil {
+			return err
+		}
+	}
 
 	return nil
 }
diff --git a/shared/container.go b/shared/container.go
index cb75ec3..d4bb6da 100644
--- a/shared/container.go
+++ b/shared/container.go
@@ -24,9 +24,16 @@ type ContainerExecControl struct {
 	Args    map[string]string `json:"args"`
 }
 
+type SnapshotState struct {
+	CreationDate int64  `json:"creation_date"`
+	Name         string `json:"name"`
+	Stateful     bool   `json:"stateful"`
+}
+
 type ContainerState struct {
 	Architecture    int               `json:"architecture"`
 	Config          map[string]string `json:"config"`
+	CreationDate    int64             `json:"creation_date"`
 	Devices         Devices           `json:"devices"`
 	Ephemeral       bool              `json:"ephemeral"`
 	ExpandedConfig  map[string]string `json:"expanded_config"`
diff --git a/specs/rest-api.md b/specs/rest-api.md
index 017ece0..0ca83db 100644
--- a/specs/rest-api.md
+++ b/specs/rest-api.md
@@ -385,6 +385,8 @@ Output:
         'profiles': ["default"],
         'architecture': 2,
         'config': {"limits.cpu": "3"},
+        'ephemeral': false,
+        'creation_date': 1455136027,
         'expanded_config': {"limits.cpu": "3"}  # the result of expanding profiles and adding the container's local config
         'devices': {
             'rootfs': {
@@ -572,6 +574,7 @@ Input:
 Return:
 
     {
+        'creation_date': 1455139453,
         'name': "my-snapshot",
         'stateful': true
     }
diff --git a/test/suites/basic.sh b/test/suites/basic.sh
index 0a1e31a..c24d53a 100644
--- a/test/suites/basic.sh
+++ b/test/suites/basic.sh
@@ -44,7 +44,7 @@ test_basic_usage() {
 
   # Test container creation
   lxc init testimage foo
-  lxc list | grep foo | grep STOPPED
+  lxc list | grep foo | grep STOPPED || bash
   lxc list fo | grep foo | grep STOPPED
 
   # Test container rename

From 87408f9b9c49fda54df4a494e8d5fdb7f1d60e03 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 10 Feb 2016 16:01:16 -0500
Subject: [PATCH 6/6] Rework snapshot rendering
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

 - Don't print the awkward list in "lxc list" anymore.
 - Update "lxc info" to show:
   - Snapshot name
   - Snapshot creation date (if known)
   - Snapshot type (stateful or stateless)

Closes #1560

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 client.go   | 16 +++-------------
 lxc/info.go | 22 +++++++++++++++++++++-
 lxc/list.go | 15 ++-------------
 po/lxd.pot  | 61 +++++++++++++++++++++++++++++++++++++------------------------
 4 files changed, 63 insertions(+), 51 deletions(-)

diff --git a/client.go b/client.go
index 136b96c..45eb1a2 100644
--- a/client.go
+++ b/client.go
@@ -1624,30 +1624,20 @@ func (c *Client) Snapshot(container string, snapshotName string, stateful bool)
 	return c.post(fmt.Sprintf("containers/%s/snapshots", container), body, Async)
 }
 
-func (c *Client) ListSnapshots(container string) ([]string, error) {
+func (c *Client) ListSnapshots(container string) ([]shared.SnapshotState, error) {
 	qUrl := fmt.Sprintf("containers/%s/snapshots?recursion=1", container)
 	resp, err := c.get(qUrl)
 	if err != nil {
 		return nil, err
 	}
 
-	var result []shared.Jmap
+	var result []shared.SnapshotState
 
 	if err := json.Unmarshal(resp.Metadata, &result); err != nil {
 		return nil, err
 	}
 
-	names := []string{}
-
-	for _, snapjmap := range result {
-		name, err := snapjmap.GetString("name")
-		if err != nil {
-			continue
-		}
-		names = append(names, name)
-	}
-
-	return names, nil
+	return result, nil
 }
 
 func (c *Client) GetServerConfigString() ([]string, error) {
diff --git a/lxc/info.go b/lxc/info.go
index 6116341..ba644e1 100644
--- a/lxc/info.go
+++ b/lxc/info.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"io/ioutil"
 	"strings"
+	"time"
 
 	"gopkg.in/yaml.v2"
 
@@ -76,7 +77,13 @@ func containerInfo(d *lxd.Client, name string, showLog bool) error {
 		return err
 	}
 
+	const layout = "2006/01/02 15:04 UTC"
+
 	fmt.Printf(i18n.G("Name: %s")+"\n", ct.Name)
+	if ct.CreationDate != 0 {
+		fmt.Printf(i18n.G("Created: %s")+"\n", time.Unix(ct.CreationDate, 0).UTC().Format(layout))
+	}
+
 	fmt.Printf(i18n.G("Status: %s")+"\n", ct.Status.Status)
 	if ct.Ephemeral {
 		fmt.Printf(i18n.G("Type: ephemeral") + "\n")
@@ -109,11 +116,24 @@ func containerInfo(d *lxd.Client, name string, showLog bool) error {
 	if err != nil {
 		return nil
 	}
+
 	for _, snap := range snaps {
 		if first_snapshot {
 			fmt.Println(i18n.G("Snapshots:"))
 		}
-		fmt.Printf("  %s\n", snap)
+		fmt.Printf("  %s", snap.Name)
+
+		if snap.CreationDate != 0 {
+			fmt.Printf(" ("+i18n.G("taken at %s")+")", time.Unix(snap.CreationDate, 0).UTC().Format(layout))
+		}
+
+		if snap.Stateful {
+			fmt.Printf(" (" + i18n.G("stateful") + ")")
+		} else {
+			fmt.Printf(" (" + i18n.G("stateless") + ")")
+		}
+		fmt.Printf("\n")
+
 		first_snapshot = false
 	}
 
diff --git a/lxc/list.go b/lxc/list.go
index 349aeef..ee25e73 100644
--- a/lxc/list.go
+++ b/lxc/list.go
@@ -149,7 +149,7 @@ func shouldShow(filters []string, state *shared.ContainerState) bool {
 	return true
 }
 
-func listContainers(cinfos []shared.ContainerInfo, filters []string, columns []Column, listsnaps bool) error {
+func listContainers(cinfos []shared.ContainerInfo, filters []string, columns []Column) error {
 	headers := []string{}
 	for _, column := range columns {
 		headers = append(headers, column.Name)
@@ -175,17 +175,6 @@ func listContainers(cinfos []shared.ContainerInfo, filters []string, columns []C
 	table.AppendBulk(data)
 	table.Render()
 
-	if listsnaps && len(cinfos) == 1 {
-		csnaps := cinfos[0].Snaps
-		first_snapshot := true
-		for _, snap := range csnaps {
-			if first_snapshot {
-				fmt.Println(i18n.G("Snapshots:"))
-			}
-			fmt.Printf("  %s\n", snap)
-			first_snapshot = false
-		}
-	}
 	return nil
 }
 
@@ -250,7 +239,7 @@ func (c *listCmd) run(config *lxd.Config, args []string) error {
 		}
 	}
 
-	return listContainers(cts, filters, columns, len(cts) == 1)
+	return listContainers(cts, filters, columns)
 }
 
 func nameColumnData(cinfo shared.ContainerInfo) string {
diff --git a/po/lxd.pot b/po/lxd.pot
index 2afd38d..07753b4 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: 2016-02-10 14:55-0500\n"
+        "POT-Creation-Date: 2016-02-10 16:04-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"
@@ -74,7 +74,7 @@ msgstr  ""
 msgid   "'/' not allowed in snapshot name"
 msgstr  ""
 
-#: lxc/info.go:102 lxc/profile.go:221
+#: lxc/info.go:109 lxc/profile.go:221
 msgid   "(none)"
 msgstr  ""
 
@@ -208,7 +208,7 @@ msgid   "Create a read-only snapshot of a container.\n"
         "lxc snapshot u1 snap0"
 msgstr  ""
 
-#: lxc/image.go:269
+#: lxc/image.go:269 lxc/info.go:84
 #, c-format
 msgid   "Created: %s"
 msgstr  ""
@@ -244,7 +244,7 @@ msgstr  ""
 msgid   "Device %s removed from %s"
 msgstr  ""
 
-#: lxc/list.go:239
+#: lxc/list.go:228
 msgid   "EPHEMERAL"
 msgstr  ""
 
@@ -322,11 +322,11 @@ msgstr  ""
 msgid   "Generating a client certificate. This may take a minute..."
 msgstr  ""
 
-#: lxc/list.go:237
+#: lxc/list.go:226
 msgid   "IPV4"
 msgstr  ""
 
-#: lxc/list.go:238
+#: lxc/list.go:227
 msgid   "IPV6"
 msgstr  ""
 
@@ -347,7 +347,7 @@ msgstr  ""
 msgid   "Image imported with fingerprint: %s"
 msgstr  ""
 
-#: lxc/info.go:88
+#: lxc/info.go:95
 #, c-format
 msgid   "Init: %d"
 msgstr  ""
@@ -380,7 +380,7 @@ msgstr  ""
 msgid   "Invalid target %s"
 msgstr  ""
 
-#: lxc/info.go:90
+#: lxc/info.go:97
 msgid   "Ips:"
 msgstr  ""
 
@@ -402,7 +402,7 @@ msgid   "Launch a container from a particular image.\n"
         "lxc launch ubuntu u1"
 msgstr  ""
 
-#: lxc/info.go:24
+#: lxc/info.go:25
 msgid   "List information on containers.\n"
         "\n"
         "This will support remotes and images as well, but only containers for now.\n"
@@ -433,7 +433,7 @@ msgid   "Lists the available resources.\n"
         "* p - pid of container init process"
 msgstr  ""
 
-#: lxc/info.go:131
+#: lxc/info.go:151
 msgid   "Log:"
 msgstr  ""
 
@@ -624,15 +624,15 @@ msgid   "Move containers within or in between lxd instances.\n"
         "    Rename a local container.\n"
 msgstr  ""
 
-#: lxc/list.go:235 lxc/remote.go:271
+#: lxc/list.go:224 lxc/remote.go:271
 msgid   "NAME"
 msgstr  ""
 
-#: lxc/list.go:304 lxc/remote.go:257
+#: lxc/list.go:293 lxc/remote.go:257
 msgid   "NO"
 msgstr  ""
 
-#: lxc/info.go:79
+#: lxc/info.go:82
 #, c-format
 msgid   "Name: %s"
 msgstr  ""
@@ -666,7 +666,7 @@ msgstr  ""
 msgid   "Override the terminal mode (auto, interactive or non-interactive)"
 msgstr  ""
 
-#: lxc/list.go:241
+#: lxc/list.go:230
 msgid   "PID"
 msgstr  ""
 
@@ -718,7 +718,7 @@ msgid   "Prints the version number of LXD.\n"
         "lxc version"
 msgstr  ""
 
-#: lxc/info.go:89
+#: lxc/info.go:96
 #, c-format
 msgid   "Processcount: %d"
 msgstr  ""
@@ -742,7 +742,7 @@ msgstr  ""
 msgid   "Profile to apply to the new container"
 msgstr  ""
 
-#: lxc/info.go:86
+#: lxc/info.go:93
 #, c-format
 msgid   "Profiles: %s"
 msgstr  ""
@@ -788,11 +788,11 @@ msgstr  ""
 msgid   "SIZE"
 msgstr  ""
 
-#: lxc/list.go:240
+#: lxc/list.go:229
 msgid   "SNAPSHOTS"
 msgstr  ""
 
-#: lxc/list.go:236
+#: lxc/list.go:225
 msgid   "STATE"
 msgstr  ""
 
@@ -833,7 +833,7 @@ msgstr  ""
 msgid   "Show all commands (not just interesting ones)"
 msgstr  ""
 
-#: lxc/info.go:33
+#: lxc/info.go:34
 msgid   "Show the container's last 100 log lines?"
 msgstr  ""
 
@@ -842,7 +842,7 @@ msgstr  ""
 msgid   "Size: %.2fMB"
 msgstr  ""
 
-#: lxc/info.go:114 lxc/list.go:183
+#: lxc/info.go:122
 msgid   "Snapshots:"
 msgstr  ""
 
@@ -851,7 +851,7 @@ msgstr  ""
 msgid   "Starting %s"
 msgstr  ""
 
-#: lxc/info.go:80
+#: lxc/info.go:87
 #, c-format
 msgid   "Status: %s"
 msgstr  ""
@@ -881,11 +881,11 @@ msgstr  ""
 msgid   "Try `lxc info --show-log %s` for more info"
 msgstr  ""
 
-#: lxc/info.go:82
+#: lxc/info.go:89
 msgid   "Type: ephemeral"
 msgstr  ""
 
-#: lxc/info.go:84
+#: lxc/info.go:91
 msgid   "Type: persistent"
 msgstr  ""
 
@@ -927,7 +927,7 @@ msgstr  ""
 msgid   "Whether to show the expanded configuration"
 msgstr  ""
 
-#: lxc/list.go:302 lxc/remote.go:259
+#: lxc/list.go:291 lxc/remote.go:259
 msgid   "YES"
 msgstr  ""
 
@@ -1005,6 +1005,19 @@ msgstr  ""
 msgid   "remote %s exists as <%s>"
 msgstr  ""
 
+#: lxc/info.go:131
+msgid   "stateful"
+msgstr  ""
+
+#: lxc/info.go:133
+msgid   "stateless"
+msgstr  ""
+
+#: lxc/info.go:127
+#, c-format
+msgid   "taken at %s"
+msgstr  ""
+
 #: lxc/exec.go:158
 msgid   "unreachable return reached"
 msgstr  ""


More information about the lxc-devel mailing list