[lxc-devel] [lxd/master] Bugfixes

stgraber on Github lxc-bot at linuxcontainers.org
Wed Feb 17 23:21:55 UTC 2016


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 756 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20160217/cdf7ba11/attachment.bin>
-------------- next part --------------
From 8ed40228b01b3c3cc981f516f82d6c817d489a6d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 17 Feb 2016 00:12:34 -0500
Subject: [PATCH 01/17] Set the public flag on the server
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/api_1.0.go | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/lxd/api_1.0.go b/lxd/api_1.0.go
index 8bb4f22..29b57e7 100644
--- a/lxd/api_1.0.go
+++ b/lxd/api_1.0.go
@@ -110,6 +110,7 @@ func api10Get(d *Daemon, r *http.Request) Response {
 			"server_version":      shared.Version}
 
 		body["environment"] = env
+		body["public"] = false
 
 		serverConfig, err := d.ConfigValuesGet()
 		if err != nil {
@@ -129,6 +130,7 @@ func api10Get(d *Daemon, r *http.Request) Response {
 		body["config"] = config
 	} else {
 		body["auth"] = "untrusted"
+		body["public"] = false
 	}
 
 	return SyncResponse(true, body)

From 2786a626683f4869ffc69b312ae98e1a7ede4dc7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 17 Feb 2016 01:54:28 -0500
Subject: [PATCH 02/17] Rework container state API
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Returning all the runtime state all the time is very costly, it's
actually better to only query and return it on demand.

So only return the basic container state (running/stopped/...) with the
container info and require accessing /state for anything else.

This will be even more important as we add cgroup resource reporting and
other bits of data in there soon.

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 client.go              | 41 +++++++++++++++++++++----------
 lxc/config.go          | 24 +++++++++----------
 lxc/copy.go            |  6 ++---
 lxc/delete.go          |  4 ++--
 lxc/info.go            | 17 ++++++++-----
 lxc/list.go            | 65 +++++++++++++++++++++++++++++---------------------
 lxc/list_test.go       |  2 +-
 lxd/container.go       |  1 +
 lxd/container_get.go   |  2 +-
 lxd/container_lxc.go   | 40 +++++++++++++++++++++----------
 lxd/container_state.go |  2 +-
 lxd/container_test.go  |  2 +-
 lxd/containers.go      | 54 +++++++++++++++++++++++++++++++----------
 lxd/containers_get.go  | 33 +++++++------------------
 lxd/main.go            | 15 ++++++++----
 shared/container.go    | 51 +++++++++------------------------------
 shared/server.go       |  2 +-
 17 files changed, 198 insertions(+), 163 deletions(-)

diff --git a/client.go b/client.go
index 16b06f6..6b498eb 100644
--- a/client.go
+++ b/client.go
@@ -1332,8 +1332,8 @@ func (c *Client) Exec(name string, cmd []string, env map[string]string,
 
 func (c *Client) Action(name string, action shared.ContainerAction, timeout int, force bool) (*Response, error) {
 	if action == "start" {
-		current, err := c.ContainerStatus(name)
-		if err == nil && current.Status.StatusCode == shared.Frozen {
+		current, err := c.ContainerState(name)
+		if err == nil && current.StatusCode == shared.Frozen {
 			action = "unfreeze"
 		}
 	}
@@ -1369,8 +1369,8 @@ func (c *Client) ServerStatus() (*shared.ServerState, error) {
 	return &ss, nil
 }
 
-func (c *Client) ContainerStatus(name string) (*shared.ContainerState, error) {
-	ct := shared.ContainerState{}
+func (c *Client) ContainerInfo(name string) (*shared.ContainerInfo, error) {
+	ct := shared.ContainerInfo{}
 
 	resp, err := c.get(fmt.Sprintf("containers/%s", name))
 	if err != nil {
@@ -1384,6 +1384,21 @@ func (c *Client) ContainerStatus(name string) (*shared.ContainerState, error) {
 	return &ct, nil
 }
 
+func (c *Client) ContainerState(name string) (*shared.ContainerState, error) {
+	ct := shared.ContainerState{}
+
+	resp, err := c.get(fmt.Sprintf("containers/%s/state", name))
+	if err != nil {
+		return nil, err
+	}
+
+	if err := json.Unmarshal(resp.Metadata, &ct); err != nil {
+		return nil, err
+	}
+
+	return &ct, nil
+}
+
 func (c *Client) GetLog(container string, log string) (io.Reader, error) {
 	uri := c.url(shared.APIVersion, "containers", container, "logs", log)
 	resp, err := c.getRaw(uri)
@@ -1543,14 +1558,14 @@ 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) ([]shared.SnapshotState, error) {
+func (c *Client) ListSnapshots(container string) ([]shared.SnapshotInfo, error) {
 	qUrl := fmt.Sprintf("containers/%s/snapshots?recursion=1", container)
 	resp, err := c.get(qUrl)
 	if err != nil {
 		return nil, err
 	}
 
-	var result []shared.SnapshotState
+	var result []shared.SnapshotInfo
 
 	if err := json.Unmarshal(resp.Metadata, &result); err != nil {
 		return nil, err
@@ -1611,7 +1626,7 @@ func (c *Client) UpdateServerConfig(ss shared.BriefServerState) (*Response, erro
 func (c *Client) GetContainerConfig(container string) ([]string, error) {
 	var resp []string
 
-	st, err := c.ContainerStatus(container)
+	st, err := c.ContainerInfo(container)
 	if err != nil {
 		return resp, err
 	}
@@ -1629,7 +1644,7 @@ func (c *Client) GetContainerConfig(container string) ([]string, error) {
 }
 
 func (c *Client) SetContainerConfig(container, key, value string) error {
-	st, err := c.ContainerStatus(container)
+	st, err := c.ContainerInfo(container)
 	if err != nil {
 		return err
 	}
@@ -1654,7 +1669,7 @@ func (c *Client) SetContainerConfig(container, key, value string) error {
 	return c.WaitForSuccess(resp.Operation)
 }
 
-func (c *Client) UpdateContainerConfig(container string, st shared.BriefContainerState) error {
+func (c *Client) UpdateContainerConfig(container string, st shared.BriefContainerInfo) error {
 	body := shared.Jmap{"name": container,
 		"profiles":  st.Profiles,
 		"config":    st.Config,
@@ -1754,7 +1769,7 @@ func (c *Client) ListProfiles() ([]string, error) {
 }
 
 func (c *Client) ApplyProfile(container, profile string) (*Response, error) {
-	st, err := c.ContainerStatus(container)
+	st, err := c.ContainerInfo(container)
 	if err != nil {
 		return nil, err
 	}
@@ -1765,7 +1780,7 @@ func (c *Client) ApplyProfile(container, profile string) (*Response, error) {
 }
 
 func (c *Client) ContainerDeviceDelete(container, devname string) (*Response, error) {
-	st, err := c.ContainerStatus(container)
+	st, err := c.ContainerInfo(container)
 	if err != nil {
 		return nil, err
 	}
@@ -1777,7 +1792,7 @@ func (c *Client) ContainerDeviceDelete(container, devname string) (*Response, er
 }
 
 func (c *Client) ContainerDeviceAdd(container, devname, devtype string, props []string) (*Response, error) {
-	st, err := c.ContainerStatus(container)
+	st, err := c.ContainerInfo(container)
 	if err != nil {
 		return nil, err
 	}
@@ -1806,7 +1821,7 @@ func (c *Client) ContainerDeviceAdd(container, devname, devtype string, props []
 }
 
 func (c *Client) ContainerListDevices(container string) ([]string, error) {
-	st, err := c.ContainerStatus(container)
+	st, err := c.ContainerInfo(container)
 	if err != nil {
 		return nil, err
 	}
diff --git a/lxc/config.go b/lxc/config.go
index d0d14d6..17c31b0 100644
--- a/lxc/config.go
+++ b/lxc/config.go
@@ -115,7 +115,7 @@ func (c *configCmd) doSet(config *lxd.Config, args []string, unset bool) error {
 	}
 
 	if unset {
-		st, err := d.ContainerStatus(container)
+		st, err := d.ContainerInfo(container)
 		if err != nil {
 			return err
 		}
@@ -335,17 +335,17 @@ func (c *configCmd) run(config *lxd.Config, args []string) error {
 				return err
 			}
 
-			brief := config.BriefState()
+			brief := config.Brief()
 			data, err = yaml.Marshal(&brief)
 		} else {
-			config, err := d.ContainerStatus(container)
+			config, err := d.ContainerInfo(container)
 			if err != nil {
 				return err
 			}
 
-			brief := config.BriefState()
+			brief := config.Brief()
 			if c.expanded {
-				brief = config.BriefStateExpanded()
+				brief = config.BriefExpanded()
 			}
 			data, err = yaml.Marshal(&brief)
 		}
@@ -373,7 +373,7 @@ func (c *configCmd) run(config *lxd.Config, args []string) error {
 		}
 
 		if container != "" {
-			resp, err := d.ContainerStatus(container)
+			resp, err := d.ContainerInfo(container)
 			if err != nil {
 				return err
 			}
@@ -452,7 +452,7 @@ func (c *configCmd) doContainerConfigEdit(client *lxd.Client, cont string) error
 			return err
 		}
 
-		newdata := shared.BriefContainerState{}
+		newdata := shared.BriefContainerInfo{}
 		err = yaml.Unmarshal(contents, &newdata)
 		if err != nil {
 			return err
@@ -461,12 +461,12 @@ func (c *configCmd) doContainerConfigEdit(client *lxd.Client, cont string) error
 	}
 
 	// Extract the current value
-	config, err := client.ContainerStatus(cont)
+	config, err := client.ContainerInfo(cont)
 	if err != nil {
 		return err
 	}
 
-	brief := config.BriefState()
+	brief := config.Brief()
 	data, err := yaml.Marshal(&brief)
 	if err != nil {
 		return err
@@ -480,7 +480,7 @@ func (c *configCmd) doContainerConfigEdit(client *lxd.Client, cont string) error
 
 	for {
 		// Parse the text received from the editor
-		newdata := shared.BriefContainerState{}
+		newdata := shared.BriefContainerInfo{}
 		err = yaml.Unmarshal(content, &newdata)
 		if err == nil {
 			err = client.UpdateContainerConfig(cont, newdata)
@@ -531,7 +531,7 @@ func (c *configCmd) doDaemonConfigEdit(client *lxd.Client) error {
 		return err
 	}
 
-	brief := config.BriefState()
+	brief := config.Brief()
 	data, err := yaml.Marshal(&brief)
 	if err != nil {
 		return err
@@ -681,7 +681,7 @@ func (c *configCmd) deviceShow(config *lxd.Config, which string, args []string)
 
 		devices = resp.Devices
 	} else {
-		resp, err := client.ContainerStatus(name)
+		resp, err := client.ContainerInfo(name)
 		if err != nil {
 			return err
 		}
diff --git a/lxc/copy.go b/lxc/copy.go
index f01a34d..4995194 100644
--- a/lxc/copy.go
+++ b/lxc/copy.go
@@ -47,7 +47,7 @@ func (c *copyCmd) copyContainer(config *lxd.Config, sourceResource string, destR
 		return err
 	}
 
-	status := &shared.ContainerState{}
+	status := &shared.ContainerInfo{}
 
 	// TODO: presumably we want to do this for copying snapshots too? We
 	// need to think a bit more about how we track the baseImage in the
@@ -56,7 +56,7 @@ func (c *copyCmd) copyContainer(config *lxd.Config, sourceResource string, destR
 	baseImage := ""
 
 	if !shared.IsSnapshot(sourceName) {
-		status, err = source.ContainerStatus(sourceName)
+		status, err = source.ContainerInfo(sourceName)
 		if err != nil {
 			return err
 		}
@@ -101,7 +101,7 @@ func (c *copyCmd) copyContainer(config *lxd.Config, sourceResource string, destR
 		}
 
 		if ephemeral == -1 {
-			ct, err := source.ContainerStatus(sourceName)
+			ct, err := source.ContainerInfo(sourceName)
 			if err != nil {
 				return err
 			}
diff --git a/lxc/delete.go b/lxc/delete.go
index d09498d..a9de74f 100644
--- a/lxc/delete.go
+++ b/lxc/delete.go
@@ -73,12 +73,12 @@ func (c *deleteCmd) run(config *lxd.Config, args []string) error {
 			return c.doDelete(d, name)
 		}
 
-		ct, err := d.ContainerStatus(name)
+		ct, err := d.ContainerInfo(name)
 		if err != nil {
 			return err
 		}
 
-		if ct.Status.StatusCode != 0 && ct.Status.StatusCode != shared.Stopped {
+		if ct.StatusCode != 0 && ct.StatusCode != shared.Stopped {
 			if !c.force {
 				return fmt.Errorf(i18n.G("The container is currently running, stop it first or pass --force."))
 			}
diff --git a/lxc/info.go b/lxc/info.go
index 1eb867e..184e899 100644
--- a/lxc/info.go
+++ b/lxc/info.go
@@ -72,7 +72,12 @@ func (c *infoCmd) remoteInfo(d *lxd.Client) error {
 }
 
 func (c *infoCmd) containerInfo(d *lxd.Client, name string, showLog bool) error {
-	ct, err := d.ContainerStatus(name)
+	ct, err := d.ContainerInfo(name)
+	if err != nil {
+		return err
+	}
+
+	cs, err := d.ContainerState(name)
 	if err != nil {
 		return err
 	}
@@ -84,19 +89,19 @@ func (c *infoCmd) containerInfo(d *lxd.Client, name string, showLog bool) error
 		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)
+	fmt.Printf(i18n.G("Status: %s")+"\n", ct.Status)
 	if ct.Ephemeral {
 		fmt.Printf(i18n.G("Type: ephemeral") + "\n")
 	} else {
 		fmt.Printf(i18n.G("Type: persistent") + "\n")
 	}
 	fmt.Printf(i18n.G("Profiles: %s")+"\n", strings.Join(ct.Profiles, ", "))
-	if ct.Status.Init != 0 {
-		fmt.Printf(i18n.G("Init: %d")+"\n", ct.Status.Init)
-		fmt.Printf(i18n.G("Processcount: %d")+"\n", ct.Status.Processcount)
+	if cs.Init != 0 {
+		fmt.Printf(i18n.G("Init: %d")+"\n", cs.Init)
+		fmt.Printf(i18n.G("Processcount: %d")+"\n", cs.Processcount)
 		fmt.Printf(i18n.G("Ips:") + "\n")
 		foundone := false
-		for _, ip := range ct.Status.Ips {
+		for _, ip := range cs.Ips {
 			vethStr := ""
 			if ip.HostVeth != "" {
 				vethStr = fmt.Sprintf("\t%s", ip.HostVeth)
diff --git a/lxc/list.go b/lxc/list.go
index 7d016c6..2364f7f 100644
--- a/lxc/list.go
+++ b/lxc/list.go
@@ -20,7 +20,7 @@ type Column struct {
 	Data columnData
 }
 
-type columnData func(shared.ContainerInfo) string
+type columnData func(shared.ContainerInfo, shared.ContainerState, []shared.SnapshotInfo) string
 
 type ByName [][]string
 
@@ -98,7 +98,7 @@ func (c *listCmd) dotPrefixMatch(short string, full string) bool {
 	return true
 }
 
-func (c *listCmd) shouldShow(filters []string, state *shared.ContainerState) bool {
+func (c *listCmd) shouldShow(filters []string, state *shared.ContainerInfo) bool {
 	for _, filter := range filters {
 		if strings.Contains(filter, "=") {
 			membs := strings.SplitN(filter, "=", 2)
@@ -149,22 +149,33 @@ func (c *listCmd) shouldShow(filters []string, state *shared.ContainerState) boo
 	return true
 }
 
-func (c *listCmd) listContainers(cinfos []shared.ContainerInfo, filters []string, columns []Column) error {
+func (c *listCmd) listContainers(d *lxd.Client, cinfos []shared.ContainerInfo, filters []string, columns []Column) error {
 	headers := []string{}
 	for _, column := range columns {
 		headers = append(headers, column.Name)
 	}
 
 	data := [][]string{}
-	for _, cinfo := range cinfos {
-		if !c.shouldShow(filters, &cinfo.State) {
+	for _, cInfo := range cinfos {
+		cState, err := d.ContainerState(cInfo.Name)
+		if err != nil {
+			return err
+		}
+
+		cSnapshots, err := d.ListSnapshots(cInfo.Name)
+		if err != nil {
+			return err
+		}
+
+		if !c.shouldShow(filters, &cInfo) {
 			continue
 		}
-		d := []string{}
+
+		col := []string{}
 		for _, column := range columns {
-			d = append(d, column.Data(cinfo))
+			col = append(col, column.Data(cInfo, *cState, cSnapshots))
 		}
-		data = append(data, d)
+		data = append(data, col)
 	}
 
 	table := tablewriter.NewWriter(os.Stdout)
@@ -214,7 +225,7 @@ func (c *listCmd) run(config *lxd.Config, args []string) error {
 		cts = ctslist
 	} else {
 		for _, cinfo := range ctslist {
-			if len(cinfo.State.Name) >= len(name) && cinfo.State.Name[0:len(name)] == name {
+			if len(cinfo.Name) >= len(name) && cinfo.Name[0:len(name)] == name {
 				cts = append(cts, cinfo)
 			}
 		}
@@ -239,21 +250,21 @@ func (c *listCmd) run(config *lxd.Config, args []string) error {
 		}
 	}
 
-	return c.listContainers(cts, filters, columns)
+	return c.listContainers(d, cts, filters, columns)
 }
 
-func (c *listCmd) nameColumnData(cinfo shared.ContainerInfo) string {
-	return cinfo.State.Name
+func (c *listCmd) nameColumnData(cInfo shared.ContainerInfo, cState shared.ContainerState, cSnaps []shared.SnapshotInfo) string {
+	return cInfo.Name
 }
 
-func (c *listCmd) statusColumnData(cinfo shared.ContainerInfo) string {
-	return strings.ToUpper(cinfo.State.Status.Status)
+func (c *listCmd) statusColumnData(cInfo shared.ContainerInfo, cState shared.ContainerState, cSnaps []shared.SnapshotInfo) string {
+	return strings.ToUpper(cInfo.Status)
 }
 
-func (c *listCmd) IP4ColumnData(cinfo shared.ContainerInfo) string {
-	if cinfo.State.Status.StatusCode == shared.Running || cinfo.State.Status.StatusCode == shared.Frozen {
+func (c *listCmd) IP4ColumnData(cInfo shared.ContainerInfo, cState shared.ContainerState, cSnaps []shared.SnapshotInfo) string {
+	if cInfo.StatusCode == shared.Running || cInfo.StatusCode == shared.Frozen {
 		ipv4s := []string{}
-		for _, ip := range cinfo.State.Status.Ips {
+		for _, ip := range cState.Ips {
 			if ip.Interface == "lo" {
 				continue
 			}
@@ -268,10 +279,10 @@ func (c *listCmd) IP4ColumnData(cinfo shared.ContainerInfo) string {
 	}
 }
 
-func (c *listCmd) IP6ColumnData(cinfo shared.ContainerInfo) string {
-	if cinfo.State.Status.StatusCode == shared.Running || cinfo.State.Status.StatusCode == shared.Frozen {
+func (c *listCmd) IP6ColumnData(cInfo shared.ContainerInfo, cState shared.ContainerState, cSnaps []shared.SnapshotInfo) string {
+	if cInfo.StatusCode == shared.Running || cInfo.StatusCode == shared.Frozen {
 		ipv6s := []string{}
-		for _, ip := range cinfo.State.Status.Ips {
+		for _, ip := range cState.Ips {
 			if ip.Interface == "lo" {
 				continue
 			}
@@ -286,21 +297,21 @@ func (c *listCmd) IP6ColumnData(cinfo shared.ContainerInfo) string {
 	}
 }
 
-func (c *listCmd) isEphemeralColumnData(cinfo shared.ContainerInfo) string {
-	if cinfo.State.Ephemeral {
+func (c *listCmd) isEphemeralColumnData(cInfo shared.ContainerInfo, cState shared.ContainerState, cSnaps []shared.SnapshotInfo) string {
+	if cInfo.Ephemeral {
 		return i18n.G("YES")
 	} else {
 		return i18n.G("NO")
 	}
 }
 
-func (c *listCmd) numberSnapshotsColumnData(cinfo shared.ContainerInfo) string {
-	return fmt.Sprintf("%d", len(cinfo.Snaps))
+func (c *listCmd) numberSnapshotsColumnData(cInfo shared.ContainerInfo, cState shared.ContainerState, cSnaps []shared.SnapshotInfo) string {
+	return fmt.Sprintf("%d", len(cSnaps))
 }
 
-func (c *listCmd) PIDColumnData(cinfo shared.ContainerInfo) string {
-	if cinfo.State.Status.Init != 0 {
-		return fmt.Sprintf("%d", cinfo.State.Status.Init)
+func (c *listCmd) PIDColumnData(cInfo shared.ContainerInfo, cState shared.ContainerState, cSnaps []shared.SnapshotInfo) string {
+	if cState.Init != 0 {
+		return fmt.Sprintf("%d", cState.Init)
 	} else {
 		return ""
 	}
diff --git a/lxc/list_test.go b/lxc/list_test.go
index c15349a..1202d21 100644
--- a/lxc/list_test.go
+++ b/lxc/list_test.go
@@ -21,7 +21,7 @@ func TestDotPrefixMatch(t *testing.T) {
 func TestShouldShow(t *testing.T) {
 	list := listCmd{}
 
-	state := &shared.ContainerState{
+	state := &shared.ContainerInfo{
 		Name: "foo",
 		Config: map[string]string{
 			"security.privileged": "1",
diff --git a/lxd/container.go b/lxd/container.go
index e70e2a8..8e4bd2e 100644
--- a/lxd/container.go
+++ b/lxd/container.go
@@ -335,6 +335,7 @@ type container interface {
 	FilePush(srcpath string, dstpath string, uid int, gid int, mode os.FileMode) error
 
 	// Status
+	Render() (*shared.ContainerInfo, error)
 	RenderState() (*shared.ContainerState, error)
 	IsPrivileged() bool
 	IsRunning() bool
diff --git a/lxd/container_get.go b/lxd/container_get.go
index 521743f..3df94ea 100644
--- a/lxd/container_get.go
+++ b/lxd/container_get.go
@@ -13,7 +13,7 @@ func containerGet(d *Daemon, r *http.Request) Response {
 		return SmartError(err)
 	}
 
-	state, err := c.RenderState()
+	state, err := c.Render()
 	if err != nil {
 		return InternalError(err)
 	}
diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index 3a22f74..866fad1 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -1341,6 +1341,31 @@ func (c *containerLXC) Unfreeze() error {
 	return c.c.Unfreeze()
 }
 
+func (c *containerLXC) Render() (*shared.ContainerInfo, error) {
+	// Load the go-lxc struct
+	err := c.initLXC()
+	if err != nil {
+		return nil, err
+	}
+
+	// FIXME: Render shouldn't directly access the go-lxc struct
+	statusCode := shared.FromLXCState(int(c.c.State()))
+
+	return &shared.ContainerInfo{
+		Architecture:    c.architecture,
+		Config:          c.localConfig,
+		CreationDate:    c.creationDate.Unix(),
+		Devices:         c.localDevices,
+		Ephemeral:       c.ephemeral,
+		ExpandedConfig:  c.expandedConfig,
+		ExpandedDevices: c.expandedDevices,
+		Name:            c.name,
+		Profiles:        c.profiles,
+		Status:          statusCode.String(),
+		StatusCode:      statusCode,
+	}, nil
+}
+
 func (c *containerLXC) RenderState() (*shared.ContainerState, error) {
 	// Load the go-lxc struct
 	err := c.initLXC()
@@ -1350,7 +1375,7 @@ func (c *containerLXC) RenderState() (*shared.ContainerState, error) {
 
 	// FIXME: RenderState shouldn't directly access the go-lxc struct
 	statusCode := shared.FromLXCState(int(c.c.State()))
-	status := shared.ContainerStatus{
+	status := shared.ContainerState{
 		Status:     statusCode.String(),
 		StatusCode: statusCode,
 	}
@@ -1362,18 +1387,7 @@ func (c *containerLXC) RenderState() (*shared.ContainerState, error) {
 		status.Ips = c.ipsGet()
 	}
 
-	return &shared.ContainerState{
-		Architecture:    c.architecture,
-		Config:          c.localConfig,
-		CreationDate:    c.creationDate.Unix(),
-		Devices:         c.localDevices,
-		Ephemeral:       c.ephemeral,
-		ExpandedConfig:  c.expandedConfig,
-		ExpandedDevices: c.expandedDevices,
-		Name:            c.name,
-		Profiles:        c.profiles,
-		Status:          status,
-	}, nil
+	return &status, nil
 }
 
 func (c *containerLXC) Snapshots() ([]container, error) {
diff --git a/lxd/container_state.go b/lxd/container_state.go
index ce7c029..9446617 100644
--- a/lxd/container_state.go
+++ b/lxd/container_state.go
@@ -28,7 +28,7 @@ func containerState(d *Daemon, r *http.Request) Response {
 		return InternalError(err)
 	}
 
-	return SyncResponse(true, state.Status)
+	return SyncResponse(true, state)
 }
 
 func containerStatePut(d *Daemon, r *http.Request) Response {
diff --git a/lxd/container_test.go b/lxd/container_test.go
index 25474a5..04c352c 100644
--- a/lxd/container_test.go
+++ b/lxd/container_test.go
@@ -80,7 +80,7 @@ func (suite *lxdTestSuite) TestContainer_ProfilesOverwriteDefaultNic() {
 
 	suite.True(c.IsPrivileged(), "This container should be privileged.")
 
-	state, err := c.RenderState()
+	state, err := c.Render()
 	suite.Req.Nil(err)
 	defer c.Delete()
 
diff --git a/lxd/containers.go b/lxd/containers.go
index aa1b2ed..520bb58 100644
--- a/lxd/containers.go
+++ b/lxd/containers.go
@@ -60,28 +60,56 @@ var containerExecCmd = Command{
 	post: containerExecPost,
 }
 
-func containersRestart(d *Daemon) error {
-	containers, err := doContainersGet(d, true)
+type containerAutostartList []container
+
+func (slice containerAutostartList) Len() int {
+	return len(slice)
+}
+
+func (slice containerAutostartList) Less(i, j int) bool {
+	iOrder := slice[i].ExpandedConfig()["boot.autostart.priority"]
+	jOrder := slice[j].ExpandedConfig()["boot.autostart.priority"]
+
+	if iOrder != jOrder {
+		iOrderInt, _ := strconv.Atoi(iOrder)
+		jOrderInt, _ := strconv.Atoi(jOrder)
+		return iOrderInt > jOrderInt
+	}
+
+	return slice[i].Name() < slice[j].Name()
+}
+
+func (slice containerAutostartList) Swap(i, j int) {
+	slice[i], slice[j] = slice[j], slice[i]
+}
 
+func containersRestart(d *Daemon) error {
+	result, err := dbContainersList(d.db, cTypeRegular)
 	if err != nil {
 		return err
 	}
 
-	containerInfo := containers.(shared.ContainerInfoList)
-	sort.Sort(containerInfo)
+	containers := []container{}
+
+	for _, name := range result {
+		c, err := containerLoadByName(d, name)
+		if err != nil {
+			return err
+		}
+
+		containers = append(containers, c)
+	}
+
+	sort.Sort(containerAutostartList(containers))
 
-	for _, container := range containerInfo {
-		lastState := container.State.Config["volatile.last_state.power"]
+	for _, c := range containers {
+		config := c.ExpandedConfig()
+		lastState := config["volatile.last_state.power"]
 
-		autoStart := container.State.ExpandedConfig["boot.autostart"]
-		autoStartDelay := container.State.ExpandedConfig["boot.autostart.delay"]
+		autoStart := config["boot.autostart"]
+		autoStartDelay := config["boot.autostart.delay"]
 
 		if lastState == "RUNNING" || autoStart == "true" {
-			c, err := containerLoadByName(d, container.State.Name)
-			if err != nil {
-				return err
-			}
-
 			if c.IsRunning() {
 				continue
 			}
diff --git a/lxd/containers_get.go b/lxd/containers_get.go
index d2a001b..547e672 100644
--- a/lxd/containers_get.go
+++ b/lxd/containers_get.go
@@ -3,7 +3,6 @@ package main
 import (
 	"fmt"
 	"net/http"
-	"strings"
 	"time"
 
 	"github.com/lxc/lxd/shared"
@@ -36,10 +35,11 @@ func doContainersGet(d *Daemon, recursion bool) (interface{}, error) {
 	}
 
 	resultString := []string{}
-	resultMap := shared.ContainerInfoList{}
+	resultList := []*shared.ContainerInfo{}
 	if err != nil {
 		return []string{}, err
 	}
+
 	for _, container := range result {
 		if !recursion {
 			url := fmt.Sprintf("/%s/containers/%s", shared.APIVersion, container)
@@ -49,7 +49,7 @@ func doContainersGet(d *Daemon, recursion bool) (interface{}, error) {
 			if response != nil {
 				continue
 			}
-			resultMap = append(resultMap, container)
+			resultList = append(resultList, container)
 		}
 	}
 
@@ -57,34 +57,19 @@ func doContainersGet(d *Daemon, recursion bool) (interface{}, error) {
 		return resultString, nil
 	}
 
-	return resultMap, nil
+	return resultList, nil
 }
 
-func doContainerGet(d *Daemon, cname string) (shared.ContainerInfo, Response) {
+func doContainerGet(d *Daemon, cname string) (*shared.ContainerInfo, Response) {
 	c, err := containerLoadByName(d, cname)
 	if err != nil {
-		return shared.ContainerInfo{}, SmartError(err)
-	}
-
-	results, err := dbContainerGetSnapshots(d.db, cname)
-	if err != nil {
-		return shared.ContainerInfo{}, SmartError(err)
+		return nil, SmartError(err)
 	}
 
-	var body []string
-
-	for _, name := range results {
-		url := fmt.Sprintf("/%s/containers/%s/snapshots/%s", shared.APIVersion, cname, strings.SplitN(name, shared.SnapshotDelimiter, 2)[1])
-		body = append(body, url)
-	}
-
-	cts, err := c.RenderState()
+	cts, err := c.Render()
 	if err != nil {
-		return shared.ContainerInfo{}, SmartError(err)
+		return nil, SmartError(err)
 	}
 
-	containerinfo := shared.ContainerInfo{State: *cts,
-		Snaps: body}
-
-	return containerinfo, nil
+	return cts, nil
 }
diff --git a/lxd/main.go b/lxd/main.go
index e18cf17..81c48f6 100644
--- a/lxd/main.go
+++ b/lxd/main.go
@@ -440,15 +440,20 @@ func activateIfNeeded() error {
 		return err
 	}
 
-	containers, err := doContainersGet(d, true)
+	result, err := dbContainersList(d.db, cTypeRegular)
 	if err != nil {
 		return err
 	}
 
-	containerInfo := containers.(shared.ContainerInfoList)
-	for _, container := range containerInfo {
-		lastState := container.State.Config["volatile.last_state.power"]
-		autoStart := container.State.ExpandedConfig["boot.autostart"]
+	for _, name := range result {
+		c, err := containerLoadByName(d, name)
+		if err != nil {
+			return err
+		}
+
+		config := c.ExpandedConfig()
+		lastState := config["volatile.last_state.power"]
+		autoStart := config["boot.autostart"]
 
 		if lastState == "RUNNING" || lastState == "Running" || autoStart == "true" {
 			shared.Debugf("Daemon has auto-started containers, activating...")
diff --git a/shared/container.go b/shared/container.go
index d4bb6da..efa8274 100644
--- a/shared/container.go
+++ b/shared/container.go
@@ -1,8 +1,6 @@
 package shared
 
-import (
-	"strconv"
-)
+import ()
 
 type Ip struct {
 	Interface string `json:"interface"`
@@ -11,7 +9,7 @@ type Ip struct {
 	HostVeth  string `json:"host_veth"`
 }
 
-type ContainerStatus struct {
+type ContainerState struct {
 	Status       string     `json:"status"`
 	StatusCode   StatusCode `json:"status_code"`
 	Init         int        `json:"init"`
@@ -24,13 +22,13 @@ type ContainerExecControl struct {
 	Args    map[string]string `json:"args"`
 }
 
-type SnapshotState struct {
+type SnapshotInfo struct {
 	CreationDate int64  `json:"creation_date"`
 	Name         string `json:"name"`
 	Stateful     bool   `json:"stateful"`
 }
 
-type ContainerState struct {
+type ContainerInfo struct {
 	Architecture    int               `json:"architecture"`
 	Config          map[string]string `json:"config"`
 	CreationDate    int64             `json:"creation_date"`
@@ -40,14 +38,15 @@ type ContainerState struct {
 	ExpandedDevices Devices           `json:"expanded_devices"`
 	Name            string            `json:"name"`
 	Profiles        []string          `json:"profiles"`
-	Status          ContainerStatus   `json:"status"`
+	Status          string            `json:"status"`
+	StatusCode      StatusCode        `json:"status_code"`
 }
 
 /*
  * BriefContainerState contains a subset of the fields in
  * ContainerState, namely those which a user may update
  */
-type BriefContainerState struct {
+type BriefContainerInfo struct {
 	Name      string            `json:"name"`
 	Profiles  []string          `json:"profiles"`
 	Config    map[string]string `json:"config"`
@@ -55,8 +54,8 @@ type BriefContainerState struct {
 	Ephemeral bool              `json:"ephemeral"`
 }
 
-func (c *ContainerState) BriefState() BriefContainerState {
-	retstate := BriefContainerState{Name: c.Name,
+func (c *ContainerInfo) Brief() BriefContainerInfo {
+	retstate := BriefContainerInfo{Name: c.Name,
 		Profiles:  c.Profiles,
 		Config:    c.Config,
 		Devices:   c.Devices,
@@ -64,8 +63,8 @@ func (c *ContainerState) BriefState() BriefContainerState {
 	return retstate
 }
 
-func (c *ContainerState) BriefStateExpanded() BriefContainerState {
-	retstate := BriefContainerState{Name: c.Name,
+func (c *ContainerInfo) BriefExpanded() BriefContainerInfo {
+	retstate := BriefContainerInfo{Name: c.Name,
 		Profiles:  c.Profiles,
 		Config:    c.ExpandedConfig,
 		Devices:   c.ExpandedDevices,
@@ -73,34 +72,6 @@ func (c *ContainerState) BriefStateExpanded() BriefContainerState {
 	return retstate
 }
 
-type ContainerInfo struct {
-	State ContainerState `json:"state"`
-	Snaps []string       `json:"snaps"`
-}
-
-type ContainerInfoList []ContainerInfo
-
-func (slice ContainerInfoList) Len() int {
-	return len(slice)
-}
-
-func (slice ContainerInfoList) Less(i, j int) bool {
-	iOrder := slice[i].State.ExpandedConfig["boot.autostart.priority"]
-	jOrder := slice[j].State.ExpandedConfig["boot.autostart.priority"]
-
-	if iOrder != jOrder {
-		iOrderInt, _ := strconv.Atoi(iOrder)
-		jOrderInt, _ := strconv.Atoi(jOrder)
-		return iOrderInt > jOrderInt
-	}
-
-	return slice[i].State.Name < slice[j].State.Name
-}
-
-func (slice ContainerInfoList) Swap(i, j int) {
-	slice[i], slice[j] = slice[j], slice[i]
-}
-
 type ContainerAction string
 
 const (
diff --git a/shared/server.go b/shared/server.go
index fe42142..37794a9 100644
--- a/shared/server.go
+++ b/shared/server.go
@@ -28,7 +28,7 @@ type BriefServerState struct {
 	Config map[string]interface{} `json:"config"`
 }
 
-func (c *ServerState) BriefState() BriefServerState {
+func (c *ServerState) Brief() BriefServerState {
 	retstate := BriefServerState{Config: c.Config}
 	return retstate
 }

From 8e6ddad02851ed6b559b9bc8a8be559c9ad4b2b4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 17 Feb 2016 02:05:46 -0500
Subject: [PATCH 03/17] list: Only query what we need
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxc/list.go | 67 +++++++++++++++++++++++++++++++++++--------------------------
 1 file changed, 39 insertions(+), 28 deletions(-)

diff --git a/lxc/list.go b/lxc/list.go
index 2364f7f..4a2b839 100644
--- a/lxc/list.go
+++ b/lxc/list.go
@@ -16,11 +16,13 @@ import (
 )
 
 type Column struct {
-	Name string
-	Data columnData
+	Name           string
+	Data           columnData
+	NeedsState     bool
+	NeedsSnapshots bool
 }
 
-type columnData func(shared.ContainerInfo, shared.ContainerState, []shared.SnapshotInfo) string
+type columnData func(shared.ContainerInfo, *shared.ContainerState, []shared.SnapshotInfo) string
 
 type ByName [][]string
 
@@ -150,6 +152,8 @@ func (c *listCmd) shouldShow(filters []string, state *shared.ContainerInfo) bool
 }
 
 func (c *listCmd) listContainers(d *lxd.Client, cinfos []shared.ContainerInfo, filters []string, columns []Column) error {
+	var err error
+
 	headers := []string{}
 	for _, column := range columns {
 		headers = append(headers, column.Name)
@@ -157,23 +161,30 @@ func (c *listCmd) listContainers(d *lxd.Client, cinfos []shared.ContainerInfo, f
 
 	data := [][]string{}
 	for _, cInfo := range cinfos {
-		cState, err := d.ContainerState(cInfo.Name)
-		if err != nil {
-			return err
-		}
-
-		cSnapshots, err := d.ListSnapshots(cInfo.Name)
-		if err != nil {
-			return err
-		}
-
 		if !c.shouldShow(filters, &cInfo) {
 			continue
 		}
 
+		var cState *shared.ContainerState
+		var cSnapshots []shared.SnapshotInfo
+
 		col := []string{}
 		for _, column := range columns {
-			col = append(col, column.Data(cInfo, *cState, cSnapshots))
+			if column.NeedsState && cState == nil {
+				cState, err = d.ContainerState(cInfo.Name)
+				if err != nil {
+					return err
+				}
+			}
+
+			if column.NeedsSnapshots && cSnapshots == nil {
+				cSnapshots, err = d.ListSnapshots(cInfo.Name)
+				if err != nil {
+					return err
+				}
+			}
+
+			col = append(col, column.Data(cInfo, cState, cSnapshots))
 		}
 		data = append(data, col)
 	}
@@ -232,13 +243,13 @@ func (c *listCmd) run(config *lxd.Config, args []string) error {
 	}
 
 	columns_map := map[rune]Column{
-		'n': Column{i18n.G("NAME"), c.nameColumnData},
-		's': Column{i18n.G("STATE"), c.statusColumnData},
-		'4': Column{i18n.G("IPV4"), c.IP4ColumnData},
-		'6': Column{i18n.G("IPV6"), c.IP6ColumnData},
-		'e': Column{i18n.G("EPHEMERAL"), c.isEphemeralColumnData},
-		'S': Column{i18n.G("SNAPSHOTS"), c.numberSnapshotsColumnData},
-		'p': Column{i18n.G("PID"), c.PIDColumnData},
+		'n': Column{i18n.G("NAME"), c.nameColumnData, false, false},
+		's': Column{i18n.G("STATE"), c.statusColumnData, false, false},
+		'4': Column{i18n.G("IPV4"), c.IP4ColumnData, true, false},
+		'6': Column{i18n.G("IPV6"), c.IP6ColumnData, true, false},
+		'e': Column{i18n.G("EPHEMERAL"), c.isEphemeralColumnData, false, false},
+		'S': Column{i18n.G("SNAPSHOTS"), c.numberSnapshotsColumnData, false, true},
+		'p': Column{i18n.G("PID"), c.PIDColumnData, true, false},
 	}
 
 	columns := []Column{}
@@ -253,15 +264,15 @@ func (c *listCmd) run(config *lxd.Config, args []string) error {
 	return c.listContainers(d, cts, filters, columns)
 }
 
-func (c *listCmd) nameColumnData(cInfo shared.ContainerInfo, cState shared.ContainerState, cSnaps []shared.SnapshotInfo) string {
+func (c *listCmd) nameColumnData(cInfo shared.ContainerInfo, cState *shared.ContainerState, cSnaps []shared.SnapshotInfo) string {
 	return cInfo.Name
 }
 
-func (c *listCmd) statusColumnData(cInfo shared.ContainerInfo, cState shared.ContainerState, cSnaps []shared.SnapshotInfo) string {
+func (c *listCmd) statusColumnData(cInfo shared.ContainerInfo, cState *shared.ContainerState, cSnaps []shared.SnapshotInfo) string {
 	return strings.ToUpper(cInfo.Status)
 }
 
-func (c *listCmd) IP4ColumnData(cInfo shared.ContainerInfo, cState shared.ContainerState, cSnaps []shared.SnapshotInfo) string {
+func (c *listCmd) IP4ColumnData(cInfo shared.ContainerInfo, cState *shared.ContainerState, cSnaps []shared.SnapshotInfo) string {
 	if cInfo.StatusCode == shared.Running || cInfo.StatusCode == shared.Frozen {
 		ipv4s := []string{}
 		for _, ip := range cState.Ips {
@@ -279,7 +290,7 @@ func (c *listCmd) IP4ColumnData(cInfo shared.ContainerInfo, cState shared.Contai
 	}
 }
 
-func (c *listCmd) IP6ColumnData(cInfo shared.ContainerInfo, cState shared.ContainerState, cSnaps []shared.SnapshotInfo) string {
+func (c *listCmd) IP6ColumnData(cInfo shared.ContainerInfo, cState *shared.ContainerState, cSnaps []shared.SnapshotInfo) string {
 	if cInfo.StatusCode == shared.Running || cInfo.StatusCode == shared.Frozen {
 		ipv6s := []string{}
 		for _, ip := range cState.Ips {
@@ -297,7 +308,7 @@ func (c *listCmd) IP6ColumnData(cInfo shared.ContainerInfo, cState shared.Contai
 	}
 }
 
-func (c *listCmd) isEphemeralColumnData(cInfo shared.ContainerInfo, cState shared.ContainerState, cSnaps []shared.SnapshotInfo) string {
+func (c *listCmd) isEphemeralColumnData(cInfo shared.ContainerInfo, cState *shared.ContainerState, cSnaps []shared.SnapshotInfo) string {
 	if cInfo.Ephemeral {
 		return i18n.G("YES")
 	} else {
@@ -305,11 +316,11 @@ func (c *listCmd) isEphemeralColumnData(cInfo shared.ContainerInfo, cState share
 	}
 }
 
-func (c *listCmd) numberSnapshotsColumnData(cInfo shared.ContainerInfo, cState shared.ContainerState, cSnaps []shared.SnapshotInfo) string {
+func (c *listCmd) numberSnapshotsColumnData(cInfo shared.ContainerInfo, cState *shared.ContainerState, cSnaps []shared.SnapshotInfo) string {
 	return fmt.Sprintf("%d", len(cSnaps))
 }
 
-func (c *listCmd) PIDColumnData(cInfo shared.ContainerInfo, cState shared.ContainerState, cSnaps []shared.SnapshotInfo) string {
+func (c *listCmd) PIDColumnData(cInfo shared.ContainerInfo, cState *shared.ContainerState, cSnaps []shared.SnapshotInfo) string {
 	if cState.Init != 0 {
 		return fmt.Sprintf("%d", cState.Init)
 	} else {

From b5b1ddb88da79b1b37f2f0ac716d611e06295c8e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 17 Feb 2016 02:17:35 -0500
Subject: [PATCH 04/17] Fix snapshot request handling
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_snapshot.go | 23 +++++++++++------------
 1 file changed, 11 insertions(+), 12 deletions(-)

diff --git a/lxd/container_snapshot.go b/lxd/container_snapshot.go
index 063364e..8fdf95f 100644
--- a/lxd/container_snapshot.go
+++ b/lxd/container_snapshot.go
@@ -12,6 +12,11 @@ import (
 	"github.com/lxc/lxd/shared"
 )
 
+type containerSnapshotPostReq struct {
+	Name     string `json:"name"`
+	Stateful bool   `json:"stateful"`
+}
+
 func containerSnapshotsGet(d *Daemon, r *http.Request) Response {
 	recursionStr := r.FormValue("recursion")
 	recursion, err := strconv.Atoi(recursionStr)
@@ -104,26 +109,20 @@ func containerSnapshotsPost(d *Daemon, r *http.Request) Response {
 		return SmartError(err)
 	}
 
-	raw := shared.Jmap{}
-	if err := json.NewDecoder(r.Body).Decode(&raw); err != nil {
+	req := containerSnapshotPostReq{}
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		return BadRequest(err)
 	}
 
-	snapshotName, err := raw.GetString("name")
-	if err != nil || snapshotName == "" {
+	if req.Name == "" {
 		// come up with a name
 		i := nextSnapshot(d, name)
-		snapshotName = fmt.Sprintf("snap%d", i)
-	}
-
-	stateful, err := raw.GetBool("stateful")
-	if err != nil {
-		return BadRequest(err)
+		req.Name = fmt.Sprintf("snap%d", i)
 	}
 
 	fullName := name +
 		shared.SnapshotDelimiter +
-		snapshotName
+		req.Name
 
 	snapshot := func(op *operation) error {
 		config := c.ExpandedConfig()
@@ -138,7 +137,7 @@ func containerSnapshotsPost(d *Daemon, r *http.Request) Response {
 			Devices:      c.ExpandedDevices(),
 		}
 
-		_, err := containerCreateAsSnapshot(d, args, c, stateful)
+		_, err := containerCreateAsSnapshot(d, args, c, req.Stateful)
 		if err != nil {
 			return err
 		}

From ec1ef11893ec6a67498045bb0146100457442acf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 17 Feb 2016 11:24:57 -0500
Subject: [PATCH 05/17] Don't render an unused operation field in sync
 responses
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/response.go | 13 ++++++++++---
 1 file changed, 10 insertions(+), 3 deletions(-)

diff --git a/lxd/response.go b/lxd/response.go
index fbfd165..f916a20 100644
--- a/lxd/response.go
+++ b/lxd/response.go
@@ -16,7 +16,14 @@ import (
 	"github.com/lxc/lxd/shared"
 )
 
-type resp struct {
+type syncResp struct {
+	Type       lxd.ResponseType  `json:"type"`
+	Status     string            `json:"status"`
+	StatusCode shared.StatusCode `json:"status_code"`
+	Metadata   interface{}       `json:"metadata"`
+}
+
+type asyncResp struct {
 	Type       lxd.ResponseType  `json:"type"`
 	Status     string            `json:"status"`
 	StatusCode shared.StatusCode `json:"status_code"`
@@ -40,7 +47,7 @@ func (r *syncResponse) Render(w http.ResponseWriter) error {
 		status = shared.Failure
 	}
 
-	resp := resp{Type: lxd.Sync, Status: status.String(), StatusCode: status, Metadata: r.metadata}
+	resp := syncResp{Type: lxd.Sync, Status: status.String(), StatusCode: status, Metadata: r.metadata}
 	return WriteJSON(w, resp)
 }
 
@@ -154,7 +161,7 @@ func (r *operationResponse) Render(w http.ResponseWriter) error {
 		return err
 	}
 
-	body := resp{
+	body := asyncResp{
 		Type:       lxd.Async,
 		Status:     shared.OperationCreated.String(),
 		StatusCode: shared.OperationCreated,

From 965f414b3fed3c062ce23e8cf60c4b318685a2d8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 17 Feb 2016 11:30:43 -0500
Subject: [PATCH 06/17] Make the logs API respect the spec
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_logs.go | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/lxd/container_logs.go b/lxd/container_logs.go
index b8b90b4..70e801e 100644
--- a/lxd/container_logs.go
+++ b/lxd/container_logs.go
@@ -26,7 +26,7 @@ func containerLogsGet(d *Daemon, r *http.Request) Response {
 		return BadRequest(err)
 	}
 
-	result := []map[string]interface{}{}
+	result := []string{}
 
 	dents, err := ioutil.ReadDir(shared.LogPath(name))
 	if err != nil {
@@ -34,10 +34,7 @@ func containerLogsGet(d *Daemon, r *http.Request) Response {
 	}
 
 	for _, f := range dents {
-		result = append(result, map[string]interface{}{
-			"name": f.Name(),
-			"size": f.Size(),
-		})
+		result = append(result, fmt.Sprintf("/%s/containers/%s/logs/%s", shared.APIVersion, name, f.Name()))
 	}
 
 	return SyncResponse(true, result)

From 49b790d354df23e9d5b88d0addd8da5599757f23 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 17 Feb 2016 12:02:54 -0500
Subject: [PATCH 07/17] Don't move the image into place until it's been parsed
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/images.go | 34 ++++++++++++++++++----------------
 1 file changed, 18 insertions(+), 16 deletions(-)

diff --git a/lxd/images.go b/lxd/images.go
index 4f036de..f39d1e0 100644
--- a/lxd/images.go
+++ b/lxd/images.go
@@ -416,6 +416,7 @@ func getImgPostInfo(d *Daemon, r *http.Request,
 	if err != nil {
 		return info, err
 	}
+	defer os.Remove(imageTarf.Name())
 
 	if ctype == "multipart/form-data" {
 		// Parse the POST data
@@ -464,6 +465,7 @@ func getImgPostInfo(d *Daemon, r *http.Request,
 		if err != nil {
 			return info, err
 		}
+		defer os.Remove(rootfsTarf.Name())
 
 		size, err = io.Copy(io.MultiWriter(rootfsTarf, sha256), part)
 		info.Size += size
@@ -485,6 +487,14 @@ func getImgPostInfo(d *Daemon, r *http.Request,
 			return info, err
 		}
 
+		imageMeta, err = getImageMetadata(imageTarf.Name())
+		if err != nil {
+			logger.Error(
+				"Failed to get image metadata",
+				log.Ctx{"err": err})
+			return info, err
+		}
+
 		imgfname := shared.VarPath("images", info.Fingerprint)
 		err = shared.FileMove(imageTarf.Name(), imgfname)
 		if err != nil {
@@ -508,14 +518,6 @@ func getImgPostInfo(d *Daemon, r *http.Request,
 					"dest":   imgfname})
 			return info, err
 		}
-
-		imageMeta, err = getImageMetadata(imgfname)
-		if err != nil {
-			logger.Error(
-				"Failed to get image metadata",
-				log.Ctx{"err": err})
-			return info, err
-		}
 	} else {
 		post.Seek(0, 0)
 		size, err = io.Copy(io.MultiWriter(imageTarf, sha256), post)
@@ -546,6 +548,14 @@ func getImgPostInfo(d *Daemon, r *http.Request,
 			return info, err
 		}
 
+		imageMeta, err = getImageMetadata(imageTarf.Name())
+		if err != nil {
+			logger.Error(
+				"Failed to get image metadata",
+				log.Ctx{"err": err})
+			return info, err
+		}
+
 		imgfname := shared.VarPath("images", info.Fingerprint)
 		err = shared.FileMove(imageTarf.Name(), imgfname)
 		if err != nil {
@@ -557,14 +567,6 @@ func getImgPostInfo(d *Daemon, r *http.Request,
 					"dest":   imgfname})
 			return info, err
 		}
-
-		imageMeta, err = getImageMetadata(imgfname)
-		if err != nil {
-			logger.Error(
-				"Failed to get image metadata",
-				log.Ctx{"err": err})
-			return info, err
-		}
 	}
 
 	info.Architecture, _ = shared.ArchitectureId(imageMeta.Architecture)

From 85a3f54af32d67c8c262383521284838348f1b83 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 17 Feb 2016 13:30:53 -0500
Subject: [PATCH 08/17] Make images API respect spec and cleanup
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This makes the Filename and Properties paramter be respected for all
image creations.

Additionally, this moves the all the images DB code to db_images.go,
adds a couple of generic functions and deprecate some barely used types
throughout the codebase.

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxc/image.go           |   4 +-
 lxd/containers_post.go |   4 +-
 lxd/daemon_images.go   |   4 +-
 lxd/db_images.go       | 166 ++++++++++++++++++++++++++++++++++++++----
 lxd/db_test.go         |   6 +-
 lxd/images.go          | 194 ++++++++++---------------------------------------
 lxd/storage.go         |   2 +-
 shared/image.go        |  14 +---
 8 files changed, 203 insertions(+), 191 deletions(-)

diff --git a/lxc/image.go b/lxc/image.go
index a0bd2cc..e288206 100644
--- a/lxc/image.go
+++ b/lxc/image.go
@@ -481,7 +481,7 @@ func (c *imageCmd) run(config *lxd.Config, args []string) error {
 			return err
 		}
 
-		properties := info.BriefInfo()
+		properties := info.Brief()
 
 		data, err := yaml.Marshal(&properties)
 		fmt.Printf("%s", data)
@@ -609,7 +609,7 @@ func (c *imageCmd) doImageEdit(client *lxd.Client, image string) error {
 		return err
 	}
 
-	brief := config.BriefInfo()
+	brief := config.Brief()
 	data, err := yaml.Marshal(&brief)
 	if err != nil {
 		return err
diff --git a/lxd/containers_post.go b/lxd/containers_post.go
index 2dda678..c5dff5f 100644
--- a/lxd/containers_post.go
+++ b/lxd/containers_post.go
@@ -84,7 +84,7 @@ func createFromImage(d *Daemon, req *containerPostReq) Response {
 			}
 		}
 
-		imgInfo, err := dbImageGet(d.db, hash, false, false)
+		_, imgInfo, err := dbImageGet(d.db, hash, false, false)
 		if err != nil {
 			return err
 		}
@@ -162,7 +162,7 @@ func createFromMigration(d *Daemon, req *containerPostReq) Response {
 		}
 
 		var c container
-		_, err := dbImageGet(d.db, req.Source.BaseImage, false, true)
+		_, _, err := dbImageGet(d.db, req.Source.BaseImage, false, true)
 
 		/* Only create a container from an image if we're going to
 		 * rsync over the top of it. In the case of a better file
diff --git a/lxd/daemon_images.go b/lxd/daemon_images.go
index cbe933f..edfdf29 100644
--- a/lxd/daemon_images.go
+++ b/lxd/daemon_images.go
@@ -55,7 +55,7 @@ func (pt *Progress) Read(p []byte) (int, error) {
 // ImageDownload checks if we have that Image Fingerprint else
 // downloads the image from a remote server.
 func (d *Daemon) ImageDownload(op *operation, server string, certificate string, secret string, fp string, forContainer bool, directDownload bool) error {
-	if _, err := dbImageGet(d.db, fp, false, false); err == nil {
+	if _, _, err := dbImageGet(d.db, fp, false, false); err == nil {
 		shared.Log.Debug("Image already exists in the db", log.Ctx{"image": fp})
 		// already have it
 		return nil
@@ -80,7 +80,7 @@ func (d *Daemon) ImageDownload(op *operation, server string, certificate string,
 			shared.Log.Warn("Value transmitted over image lock semaphore?")
 		}
 
-		if _, err := dbImageGet(d.db, fp, false, true); err != nil {
+		if _, _, err := dbImageGet(d.db, fp, false, true); err != nil {
 			shared.Log.Error(
 				"Previous download didn't succeed",
 				log.Ctx{"image": fp})
diff --git a/lxd/db_images.go b/lxd/db_images.go
index 2cd41c7..000fbcb 100644
--- a/lxd/db_images.go
+++ b/lxd/db_images.go
@@ -36,15 +36,16 @@ func dbImagesGet(db *sql.DB, public bool) ([]string, error) {
 // pass a shortform and will get the full fingerprint.
 // There can never be more than one image with a given fingerprint, as it is
 // enforced by a UNIQUE constraint in the schema.
-func dbImageGet(db *sql.DB, fingerprint string, public bool, strictMatching bool) (*shared.ImageBaseInfo, error) {
+func dbImageGet(db *sql.DB, fingerprint string, public bool, strictMatching bool) (int, *shared.ImageInfo, error) {
 	var err error
 	var create, expire, upload *time.Time // These hold the db-returned times
 
 	// The object we'll actually return
-	image := new(shared.ImageBaseInfo)
+	image := shared.ImageInfo{}
+	id := -1
 
 	// These two humongous things will be filled by the call to DbQueryRowScan
-	outfmt := []interface{}{&image.Id, &image.Fingerprint, &image.Filename,
+	outfmt := []interface{}{&id, &image.Fingerprint, &image.Filename,
 		&image.Size, &image.Public, &image.Architecture,
 		&create, &expire, &upload}
 
@@ -78,7 +79,7 @@ func dbImageGet(db *sql.DB, fingerprint string, public bool, strictMatching bool
 	err = dbQueryRowScan(db, query, inargs, outfmt)
 
 	if err != nil {
-		return nil, err // Likely: there are no rows for this fingerprint
+		return -1, nil, err // Likely: there are no rows for this fingerprint
 	}
 
 	// Some of the dates can be nil in the DB, let's process them.
@@ -95,7 +96,45 @@ func dbImageGet(db *sql.DB, fingerprint string, public bool, strictMatching bool
 	// The upload date is enforced by NOT NULL in the schema, so it can never be nil.
 	image.UploadDate = upload.Unix()
 
-	return image, nil
+	// Get the properties
+	q := "SELECT key, value FROM images_properties where image_id=?"
+	var key, value, name, desc string
+	inargs = []interface{}{id}
+	outfmt = []interface{}{key, value}
+	results, err := dbQueryScan(db, q, inargs, outfmt)
+	if err != nil {
+		return -1, nil, err
+	}
+
+	properties := map[string]string{}
+	for _, r := range results {
+		key = r[0].(string)
+		value = r[1].(string)
+		properties[key] = value
+	}
+
+	image.Properties = properties
+
+	// Get the aliases
+	q = "SELECT name, description FROM images_aliases WHERE image_id=?"
+	inargs = []interface{}{id}
+	outfmt = []interface{}{name, desc}
+	results, err = dbQueryScan(db, q, inargs, outfmt)
+	if err != nil {
+		return -1, nil, err
+	}
+
+	aliases := shared.ImageAliases{}
+	for _, r := range results {
+		name = r[0].(string)
+		desc = r[0].(string)
+		a := shared.ImageAlias{Name: name, Description: desc}
+		aliases = append(aliases, a)
+	}
+
+	image.Aliases = aliases
+
+	return id, &image, nil
 }
 
 func dbImageDelete(db *sql.DB, id int) error {
@@ -138,15 +177,8 @@ func dbImageAliasGet(db *sql.DB, name string) (fingerprint string, err error) {
 	return fingerprint, nil
 }
 
-func dbImageSetPublic(db *sql.DB, id int, public bool) error {
-	var err error
-
-	if public {
-		_, err = dbExec(db, "UPDATE images SET public=1 WHERE id=?", id)
-	} else {
-		_, err = dbExec(db, "UPDATE images SET public=0 WHERE id=?", id)
-	}
-
+func dbImageAliasDelete(db *sql.DB, name string) error {
+	_, err := dbExec(db, "DELETE FROM images_aliases WHERE name=?", name)
 	return err
 }
 
@@ -184,3 +216,109 @@ func dbImageExpiryGet(db *sql.DB) (string, error) {
 		return "", err
 	}
 }
+
+func dbImageUpdate(db *sql.DB, id int, fname string, sz int64, public bool, arch int, creationDate int64, expiryDate int64, properties map[string]string) error {
+	tx, err := dbBegin(db)
+	if err != nil {
+		return err
+	}
+
+	sqlPublic := 0
+	if public {
+		sqlPublic = 1
+	}
+
+	stmt, err := tx.Prepare(`UPDATE images SET filename=?, size=?, public=?, architecture=?, creation_date=?, expiry_date=? WHERE id=?`)
+	if err != nil {
+		tx.Rollback()
+		return err
+	}
+	defer stmt.Close()
+
+	_, err = stmt.Exec(fname, sz, sqlPublic, arch, creationDate, expiryDate, id)
+	if err != nil {
+		tx.Rollback()
+		return err
+	}
+
+	_, err = tx.Exec(`DELETE FROM images_properties WHERE image_id=?`, id)
+
+	stmt, err = tx.Prepare(`INSERT INTO images_properties (image_id, type, key, value) VALUES (?, ?, ?, ?)`)
+	if err != nil {
+		tx.Rollback()
+		return err
+	}
+
+	for key, value := range properties {
+		_, err = stmt.Exec(id, 0, key, value)
+		if err != nil {
+			tx.Rollback()
+			return err
+		}
+	}
+
+	if err := txCommit(tx); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func dbImageInsert(db *sql.DB, fp string, fname string, sz int64, public bool, arch int, creationDate int64, expiryDate int64, properties map[string]string) error {
+	tx, err := dbBegin(db)
+	if err != nil {
+		return err
+	}
+
+	sqlPublic := 0
+	if public {
+		sqlPublic = 1
+	}
+
+	stmt, err := tx.Prepare(`INSERT INTO images (fingerprint, filename, size, public, architecture, creation_date, expiry_date, upload_date) VALUES (?, ?, ?, ?, ?, ?, ?, strftime("%s"))`)
+	if err != nil {
+		tx.Rollback()
+		return err
+	}
+	defer stmt.Close()
+
+	result, err := stmt.Exec(fp, fname, sz, sqlPublic, arch, creationDate, expiryDate)
+	if err != nil {
+		tx.Rollback()
+		return err
+	}
+
+	if len(properties) > 0 {
+		id64, err := result.LastInsertId()
+		if err != nil {
+			tx.Rollback()
+			return err
+		}
+		id := int(id64)
+
+		pstmt, err := tx.Prepare(`INSERT INTO images_properties (image_id, type, key, value) VALUES (?, 0, ?, ?)`)
+		if err != nil {
+			tx.Rollback()
+			return err
+		}
+		defer pstmt.Close()
+
+		for k, v := range properties {
+
+			// we can assume, that there is just one
+			// value per key
+			_, err = pstmt.Exec(id, k, v)
+			if err != nil {
+				tx.Rollback()
+				return err
+			}
+		}
+
+	}
+
+	if err := txCommit(tx); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/lxd/db_test.go b/lxd/db_test.go
index 4cba4bd..a917a50 100644
--- a/lxd/db_test.go
+++ b/lxd/db_test.go
@@ -398,12 +398,12 @@ INSERT INTO containers_config (container_id, key, value) VALUES (1, 'thekey', 't
 func Test_dbImageGet_finds_image_for_fingerprint(t *testing.T) {
 	var db *sql.DB
 	var err error
-	var result *shared.ImageBaseInfo
+	var result *shared.ImageInfo
 
 	db = createTestDb(t)
 	defer db.Close()
 
-	result, err = dbImageGet(db, "fingerprint", false, false)
+	_, result, err = dbImageGet(db, "fingerprint", false, false)
 
 	if err != nil {
 		t.Fatal(err)
@@ -437,7 +437,7 @@ func Test_dbImageGet_for_missing_fingerprint(t *testing.T) {
 	db = createTestDb(t)
 	defer db.Close()
 
-	_, err = dbImageGet(db, "unknown", false, false)
+	_, _, err = dbImageGet(db, "unknown", false, false)
 
 	if err != sql.ErrNoRows {
 		t.Fatal("Wrong err type returned")
diff --git a/lxd/images.go b/lxd/images.go
index f39d1e0..197e833 100644
--- a/lxd/images.go
+++ b/lxd/images.go
@@ -249,7 +249,7 @@ func imgPostContInfo(d *Daemon, r *http.Request, req imagePostReq,
 	}
 	info.Fingerprint = fmt.Sprintf("%x", sha256.Sum(nil))
 
-	_, err = dbImageGet(d.db, info.Fingerprint, false, true)
+	_, _, err = dbImageGet(d.db, info.Fingerprint, false, true)
 	if err == nil {
 		return info, fmt.Errorf("The image already exists: %s", info.Fingerprint)
 	}
@@ -295,13 +295,19 @@ func imgPostRemoteInfo(d *Daemon, req imagePostReq, op *operation) error {
 		return err
 	}
 
-	info, err := dbImageGet(d.db, hash, false, false)
+	id, info, err := dbImageGet(d.db, hash, false, false)
 	if err != nil {
 		return err
 	}
 
-	if req.Public {
-		err = dbImageSetPublic(d.db, info.Id, req.Public)
+	// Allow overriding or adding properties
+	for k, v := range req.Properties {
+		info.Properties[k] = v
+	}
+
+	// Update the DB record if needed
+	if req.Public || req.Filename != "" || len(req.Properties) > 0 {
+		err = dbImageUpdate(d.db, id, req.Filename, info.Size, req.Public, info.Architecture, info.CreationDate, info.ExpiryDate, info.Properties)
 		if err != nil {
 			return err
 		}
@@ -374,13 +380,18 @@ func imgPostURLInfo(d *Daemon, req imagePostReq, op *operation) error {
 		return err
 	}
 
-	info, err := dbImageGet(d.db, hash, false, false)
+	id, info, err := dbImageGet(d.db, hash, false, false)
 	if err != nil {
 		return err
 	}
 
-	if req.Public {
-		err = dbImageSetPublic(d.db, info.Id, req.Public)
+	// Allow overriding or adding properties
+	for k, v := range req.Properties {
+		info.Properties[k] = v
+	}
+
+	if req.Public || req.Filename != "" || len(req.Properties) > 0 {
+		err = dbImageUpdate(d.db, id, req.Filename, info.Size, req.Public, info.Architecture, info.CreationDate, info.ExpiryDate, info.Properties)
 		if err != nil {
 			return err
 		}
@@ -586,75 +597,14 @@ func getImgPostInfo(d *Daemon, r *http.Request,
 	return info, nil
 }
 
-func dbInsertImage(d *Daemon, fp string, fname string, sz int64, public bool,
-	arch int, creationDate int64, expiryDate int64, properties map[string]string) error {
-	tx, err := dbBegin(d.db)
-	if err != nil {
-		return err
-	}
-
-	sqlPublic := 0
-	if public {
-		sqlPublic = 1
-	}
-
-	stmt, err := tx.Prepare(`INSERT INTO images (fingerprint, filename, size, public, architecture, creation_date, expiry_date, upload_date) VALUES (?, ?, ?, ?, ?, ?, ?, strftime("%s"))`)
-	if err != nil {
-		tx.Rollback()
-		return err
-	}
-	defer stmt.Close()
-
-	result, err := stmt.Exec(fp, fname, sz, sqlPublic, arch, creationDate, expiryDate)
-	if err != nil {
-		tx.Rollback()
-		return err
-	}
-
-	if len(properties) > 0 {
-
-		id64, err := result.LastInsertId()
-		if err != nil {
-			tx.Rollback()
-			return err
-		}
-		id := int(id64)
-
-		pstmt, err := tx.Prepare(`INSERT INTO images_properties (image_id, type, key, value) VALUES (?, 0, ?, ?)`)
-		if err != nil {
-			tx.Rollback()
-			return err
-		}
-		defer pstmt.Close()
-
-		for k, v := range properties {
-
-			// we can assume, that there is just one
-			// value per key
-			_, err = pstmt.Exec(id, k, v)
-			if err != nil {
-				tx.Rollback()
-				return err
-			}
-		}
-
-	}
-
-	if err := txCommit(tx); err != nil {
-		return err
-	}
-
-	return nil
-}
-
 func imageBuildFromInfo(d *Daemon, info shared.ImageInfo) (metadata map[string]string, err error) {
 	err = d.Storage.ImageCreate(info.Fingerprint)
 	if err != nil {
 		return metadata, err
 	}
 
-	err = dbInsertImage(
-		d,
+	err = dbImageInsert(
+		d.db,
 		info.Fingerprint,
 		info.Filename,
 		info.Size,
@@ -818,7 +768,7 @@ func doImagesGet(d *Daemon, recursion bool, public bool) (interface{}, error) {
 	}
 
 	resultString := make([]string, len(results))
-	resultMap := make([]shared.ImageInfo, len(results))
+	resultMap := make([]*shared.ImageInfo, len(results))
 	i := 0
 	for _, name := range results {
 		if !recursion {
@@ -855,7 +805,7 @@ func imagesGet(d *Daemon, r *http.Request) Response {
 var imagesCmd = Command{name: "images", post: imagesPost, untrustedGet: true, get: imagesGet}
 
 func doDeleteImage(d *Daemon, fingerprint string) error {
-	imgInfo, err := dbImageGet(d.db, fingerprint, false, false)
+	id, imgInfo, err := dbImageGet(d.db, fingerprint, false, false)
 	if err != nil {
 		return err
 	}
@@ -891,7 +841,7 @@ func doDeleteImage(d *Daemon, fingerprint string) error {
 	}
 
 	// Remove the DB entry
-	if err = dbImageDelete(d.db, imgInfo.Id); err != nil {
+	if err = dbImageDelete(d.db, id); err != nil {
 		return err
 	}
 
@@ -908,54 +858,13 @@ func imageDelete(d *Daemon, r *http.Request) Response {
 	return EmptySyncResponse
 }
 
-func doImageGet(d *Daemon, fingerprint string, public bool) (shared.ImageInfo, Response) {
-	imgInfo, err := dbImageGet(d.db, fingerprint, public, false)
-	if err != nil {
-		return shared.ImageInfo{}, SmartError(err)
-	}
-
-	q := "SELECT key, value FROM images_properties where image_id=?"
-	var key, value, name, desc string
-	inargs := []interface{}{imgInfo.Id}
-	outfmt := []interface{}{key, value}
-	results, err := dbQueryScan(d.db, q, inargs, outfmt)
+func doImageGet(d *Daemon, fingerprint string, public bool) (*shared.ImageInfo, Response) {
+	_, imgInfo, err := dbImageGet(d.db, fingerprint, public, false)
 	if err != nil {
-		return shared.ImageInfo{}, SmartError(err)
+		return nil, SmartError(err)
 	}
-	properties := map[string]string{}
-	for _, r := range results {
-		key = r[0].(string)
-		value = r[1].(string)
-		properties[key] = value
-	}
-
-	q = "SELECT name, description FROM images_aliases WHERE image_id=?"
-	inargs = []interface{}{imgInfo.Id}
-	outfmt = []interface{}{name, desc}
-	results, err = dbQueryScan(d.db, q, inargs, outfmt)
-	if err != nil {
-		return shared.ImageInfo{}, InternalError(err)
-	}
-	aliases := shared.ImageAliases{}
-	for _, r := range results {
-		name = r[0].(string)
-		desc = r[0].(string)
-		a := shared.ImageAlias{Name: name, Description: desc}
-		aliases = append(aliases, a)
-	}
-
-	info := shared.ImageInfo{Fingerprint: imgInfo.Fingerprint,
-		Filename:     imgInfo.Filename,
-		Properties:   properties,
-		Aliases:      aliases,
-		Public:       imgInfo.Public,
-		Size:         imgInfo.Size,
-		Architecture: imgInfo.Architecture,
-		CreationDate: imgInfo.CreationDate,
-		ExpiryDate:   imgInfo.ExpiryDate,
-		UploadDate:   imgInfo.UploadDate}
 
-	return info, nil
+	return imgInfo, nil
 }
 
 func imageValidSecret(fingerprint string, secret string) bool {
@@ -1013,45 +922,19 @@ type imagePutReq struct {
 func imagePut(d *Daemon, r *http.Request) Response {
 	fingerprint := mux.Vars(r)["fingerprint"]
 
-	imageRaw := imagePutReq{}
-	if err := json.NewDecoder(r.Body).Decode(&imageRaw); err != nil {
+	req := imagePutReq{}
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		return BadRequest(err)
 	}
 
-	imgInfo, err := dbImageGet(d.db, fingerprint, false, false)
+	id, info, err := dbImageGet(d.db, fingerprint, false, false)
 	if err != nil {
 		return SmartError(err)
 	}
 
-	tx, err := dbBegin(d.db)
+	err = dbImageUpdate(d.db, id, info.Filename, info.Size, req.Public, info.Architecture, info.CreationDate, info.ExpiryDate, req.Properties)
 	if err != nil {
-		return InternalError(err)
-	}
-
-	_, err = tx.Exec(`DELETE FROM images_properties WHERE image_id=?`, imgInfo.Id)
-
-	stmt, err := tx.Prepare(`INSERT INTO images_properties (image_id, type, key, value) VALUES (?, ?, ?, ?)`)
-	if err != nil {
-		tx.Rollback()
-		return InternalError(err)
-	}
-
-	for key, value := range imageRaw.Properties {
-		_, err = stmt.Exec(imgInfo.Id, 0, key, value)
-		if err != nil {
-			tx.Rollback()
-			return InternalError(err)
-		}
-	}
-
-	if err := txCommit(tx); err != nil {
-		return InternalError(err)
-	}
-
-	err = dbImageSetPublic(d.db, imgInfo.Id, imageRaw.Public)
-	if err != nil {
-		tx.Rollback()
-		return InternalError(err)
+		return SmartError(err)
 	}
 
 	return EmptySyncResponse
@@ -1081,12 +964,12 @@ func aliasesPost(d *Daemon, r *http.Request) Response {
 		return Conflict
 	}
 
-	imgInfo, err := dbImageGet(d.db, req.Target, false, false)
+	id, _, err := dbImageGet(d.db, req.Target, false, false)
 	if err != nil {
 		return SmartError(err)
 	}
 
-	err = dbImageAliasAdd(d.db, req.Name, imgInfo.Id, req.Description)
+	err = dbImageAliasAdd(d.db, req.Name, id, req.Description)
 	if err != nil {
 		return InternalError(err)
 	}
@@ -1168,7 +1051,10 @@ func aliasDelete(d *Daemon, r *http.Request) Response {
 		return SmartError(err)
 	}
 
-	_, _ = dbExec(d.db, "DELETE FROM images_aliases WHERE name=?", name)
+	err = dbImageAliasDelete(d.db, name)
+	if err != nil {
+		return SmartError(err)
+	}
 
 	return EmptySyncResponse
 }
@@ -1183,7 +1069,7 @@ func imageExport(d *Daemon, r *http.Request) Response {
 		public = false
 	}
 
-	imgInfo, err := dbImageGet(d.db, fingerprint, public, false)
+	_, imgInfo, err := dbImageGet(d.db, fingerprint, public, false)
 	if err != nil {
 		return SmartError(err)
 	}
@@ -1223,7 +1109,7 @@ func imageExport(d *Daemon, r *http.Request) Response {
 
 func imageSecret(d *Daemon, r *http.Request) Response {
 	fingerprint := mux.Vars(r)["fingerprint"]
-	_, err := dbImageGet(d.db, fingerprint, false, false)
+	_, _, err := dbImageGet(d.db, fingerprint, false, false)
 	if err != nil {
 		return SmartError(err)
 	}
diff --git a/lxd/storage.go b/lxd/storage.go
index a77bd6e..95414c5 100644
--- a/lxd/storage.go
+++ b/lxd/storage.go
@@ -245,7 +245,7 @@ func storageForFilename(d *Daemon, filename string) (storage, error) {
 	return newStorageWithConfig(d, storageType, config)
 }
 
-func storageForImage(d *Daemon, imgInfo *shared.ImageBaseInfo) (storage, error) {
+func storageForImage(d *Daemon, imgInfo *shared.ImageInfo) (storage, error) {
 	imageFilename := shared.VarPath("images", imgInfo.Fingerprint)
 	return storageForFilename(d, imageFilename)
 }
diff --git a/shared/image.go b/shared/image.go
index 696c612..32199b5 100644
--- a/shared/image.go
+++ b/shared/image.go
@@ -32,21 +32,9 @@ type BriefImageInfo struct {
 	Public     bool              `json:"public"`
 }
 
-func (i *ImageInfo) BriefInfo() BriefImageInfo {
+func (i *ImageInfo) Brief() BriefImageInfo {
 	retstate := BriefImageInfo{
 		Properties: i.Properties,
 		Public:     i.Public}
 	return retstate
 }
-
-type ImageBaseInfo struct {
-	Id           int
-	Fingerprint  string
-	Filename     string
-	Size         int64
-	Public       bool
-	Architecture int
-	CreationDate int64
-	ExpiryDate   int64
-	UploadDate   int64
-}

From 0cfb097e48c8aee9afa8737ab1daeb5c055eb977 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 17 Feb 2016 15:10:45 -0500
Subject: [PATCH 09/17] Consistently use timestamps
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Always use time.Time internally for timestamps and always render them as
a user readable string over the API.

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxc/image.go              | 13 ++++++-------
 lxc/info.go               |  9 ++++-----
 lxd/container.go          |  4 ++--
 lxd/container_lxc.go      |  6 +++---
 lxd/container_snapshot.go | 12 ++++++------
 lxd/daemon_images.go      |  5 +++--
 lxd/db_containers.go      |  3 +--
 lxd/db_images.go          | 14 +++++++-------
 lxd/db_test.go            | 13 +++++++------
 lxd/images.go             |  9 +++++----
 shared/container.go       | 12 +++++++-----
 shared/image.go           | 10 +++++++---
 12 files changed, 58 insertions(+), 52 deletions(-)

diff --git a/lxc/image.go b/lxc/image.go
index e288206..ade0bb2 100644
--- a/lxc/image.go
+++ b/lxc/image.go
@@ -8,7 +8,6 @@ import (
 	"sort"
 	"strings"
 	"syscall"
-	"time"
 
 	"github.com/olekukonko/tablewriter"
 	"golang.org/x/crypto/ssh/terminal"
@@ -301,12 +300,12 @@ func (c *imageCmd) run(config *lxd.Config, args []string) error {
 		fmt.Printf(i18n.G("Public: %s")+"\n", public)
 		fmt.Printf(i18n.G("Timestamps:") + "\n")
 		const layout = "2006/01/02 15:04 UTC"
-		if info.CreationDate != 0 {
-			fmt.Printf("    "+i18n.G("Created: %s")+"\n", time.Unix(info.CreationDate, 0).UTC().Format(layout))
+		if info.CreationDate.UTC().Unix() != 0 {
+			fmt.Printf("    "+i18n.G("Created: %s")+"\n", info.CreationDate.UTC().Format(layout))
 		}
-		fmt.Printf("    "+i18n.G("Uploaded: %s")+"\n", time.Unix(info.UploadDate, 0).UTC().Format(layout))
-		if info.ExpiryDate != 0 {
-			fmt.Printf("    "+i18n.G("Expires: %s")+"\n", time.Unix(info.ExpiryDate, 0).UTC().Format(layout))
+		fmt.Printf("    "+i18n.G("Uploaded: %s")+"\n", info.UploadDate.UTC().Format(layout))
+		if info.ExpiryDate.UTC().Unix() != 0 {
+			fmt.Printf("    "+i18n.G("Expires: %s")+"\n", info.ExpiryDate.UTC().Format(layout))
 		} else {
 			fmt.Printf("    " + i18n.G("Expires: never") + "\n")
 		}
@@ -544,7 +543,7 @@ func (c *imageCmd) showImages(images []shared.ImageInfo, filters []string) error
 		}
 
 		const layout = "Jan 2, 2006 at 3:04pm (MST)"
-		uploaded := time.Unix(image.UploadDate, 0).Format(layout)
+		uploaded := image.UploadDate.UTC().Format(layout)
 		arch, _ := shared.ArchitectureName(image.Architecture)
 		size := fmt.Sprintf("%.2fMB", float64(image.Size)/1024.0/1024.0)
 		data = append(data, []string{shortest, fp, public, description, arch, size, uploaded})
diff --git a/lxc/info.go b/lxc/info.go
index 184e899..d5cb70d 100644
--- a/lxc/info.go
+++ b/lxc/info.go
@@ -4,7 +4,6 @@ import (
 	"fmt"
 	"io/ioutil"
 	"strings"
-	"time"
 
 	"gopkg.in/yaml.v2"
 
@@ -85,8 +84,8 @@ func (c *infoCmd) containerInfo(d *lxd.Client, name string, showLog bool) error
 	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))
+	if ct.CreationDate.UTC().Unix() != 0 {
+		fmt.Printf(i18n.G("Created: %s")+"\n", ct.CreationDate.UTC().Format(layout))
 	}
 
 	fmt.Printf(i18n.G("Status: %s")+"\n", ct.Status)
@@ -128,8 +127,8 @@ func (c *infoCmd) containerInfo(d *lxd.Client, name string, showLog bool) error
 		}
 		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.CreationDate.UTC().Unix() != 0 {
+			fmt.Printf(" ("+i18n.G("taken at %s")+")", snap.CreationDate.UTC().Format(layout))
 		}
 
 		if snap.Stateful {
diff --git a/lxd/container.go b/lxd/container.go
index 8e4bd2e..0792c17 100644
--- a/lxd/container.go
+++ b/lxd/container.go
@@ -296,7 +296,7 @@ type containerArgs struct {
 	Architecture int
 	BaseImage    string
 	Config       map[string]string
-	CreationDate *time.Time
+	CreationDate time.Time
 	Ctype        containerType
 	Devices      shared.Devices
 	Ephemeral    bool
@@ -352,7 +352,7 @@ type container interface {
 	Id() int
 	Name() string
 	Architecture() int
-	CreationDate() *time.Time
+	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 866fad1..dfe14dd 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -208,7 +208,7 @@ type containerLXC struct {
 	// Properties
 	architecture int
 	cType        containerType
-	creationDate *time.Time
+	creationDate time.Time
 	ephemeral    bool
 	id           int
 	name         string
@@ -1354,7 +1354,7 @@ func (c *containerLXC) Render() (*shared.ContainerInfo, error) {
 	return &shared.ContainerInfo{
 		Architecture:    c.architecture,
 		Config:          c.localConfig,
-		CreationDate:    c.creationDate.Unix(),
+		CreationDate:    c.creationDate,
 		Devices:         c.localDevices,
 		Ephemeral:       c.ephemeral,
 		ExpandedConfig:  c.expandedConfig,
@@ -3843,7 +3843,7 @@ func (c *containerLXC) Architecture() int {
 	return c.architecture
 }
 
-func (c *containerLXC) CreationDate() *time.Time {
+func (c *containerLXC) CreationDate() time.Time {
 	return c.creationDate
 }
 func (c *containerLXC) ExpandedConfig() map[string]string {
diff --git a/lxd/container_snapshot.go b/lxd/container_snapshot.go
index 8fdf95f..0f333e2 100644
--- a/lxd/container_snapshot.go
+++ b/lxd/container_snapshot.go
@@ -45,9 +45,9 @@ func containerSnapshotsGet(d *Daemon, r *http.Request) Response {
 			resultString = append(resultString, url)
 		} else {
 			body := shared.Jmap{
-				"name":          snapName,
-				"creation_date": snap.CreationDate().Unix(),
-				"stateful":      shared.PathExists(snap.StatePath())}
+				"name":       snapName,
+				"created_at": snap.CreationDate(),
+				"stateful":   shared.PathExists(snap.StatePath())}
 			resultMap = append(resultMap, body)
 		}
 	}
@@ -183,9 +183,9 @@ func snapshotHandler(d *Daemon, r *http.Request) Response {
 
 func snapshotGet(sc container, name string) Response {
 	body := shared.Jmap{
-		"name":          name,
-		"creation_date": sc.CreationDate().Unix(),
-		"stateful":      shared.PathExists(sc.StatePath())}
+		"name":       name,
+		"created_at": sc.CreationDate(),
+		"stateful":   shared.PathExists(sc.StatePath())}
 	return SyncResponse(true, body)
 }
 
diff --git a/lxd/daemon_images.go b/lxd/daemon_images.go
index edfdf29..655a0f9 100644
--- a/lxd/daemon_images.go
+++ b/lxd/daemon_images.go
@@ -8,6 +8,7 @@ import (
 	"mime/multipart"
 	"os"
 	"path/filepath"
+	"time"
 
 	"github.com/lxc/lxd/shared"
 
@@ -288,8 +289,8 @@ func (d *Daemon) ImageDownload(op *operation, server string, certificate string,
 		}
 
 		info.Architecture, _ = shared.ArchitectureId(imageMeta.Architecture)
-		info.CreationDate = imageMeta.CreationDate
-		info.ExpiryDate = imageMeta.ExpiryDate
+		info.CreationDate = time.Unix(imageMeta.CreationDate, 0)
+		info.ExpiryDate = time.Unix(imageMeta.ExpiryDate, 0)
 		info.Properties = imageMeta.Properties
 	}
 
diff --git a/lxd/db_containers.go b/lxd/db_containers.go
index c8a1e53..b0b8ee2 100644
--- a/lxd/db_containers.go
+++ b/lxd/db_containers.go
@@ -124,8 +124,7 @@ func dbContainerCreate(db *sql.DB, args containerArgs) (int, error) {
 		ephemInt = 1
 	}
 
-	now := time.Now().UTC()
-	args.CreationDate = &now
+	args.CreationDate = time.Now().UTC()
 
 	str := fmt.Sprintf("INSERT INTO containers (name, architecture, type, ephemeral, creation_date) VALUES (?, ?, ?, ?, ?)")
 	stmt, err := tx.Prepare(str)
diff --git a/lxd/db_images.go b/lxd/db_images.go
index 000fbcb..91348c9 100644
--- a/lxd/db_images.go
+++ b/lxd/db_images.go
@@ -84,17 +84,17 @@ func dbImageGet(db *sql.DB, fingerprint string, public bool, strictMatching bool
 
 	// Some of the dates can be nil in the DB, let's process them.
 	if create != nil {
-		image.CreationDate = create.Unix()
+		image.CreationDate = *create
 	} else {
-		image.CreationDate = 0
+		image.CreationDate = time.Time{}
 	}
 	if expire != nil {
-		image.ExpiryDate = expire.Unix()
+		image.ExpiryDate = *expire
 	} else {
-		image.ExpiryDate = 0
+		image.ExpiryDate = time.Time{}
 	}
 	// The upload date is enforced by NOT NULL in the schema, so it can never be nil.
-	image.UploadDate = upload.Unix()
+	image.UploadDate = *upload
 
 	// Get the properties
 	q := "SELECT key, value FROM images_properties where image_id=?"
@@ -217,7 +217,7 @@ func dbImageExpiryGet(db *sql.DB) (string, error) {
 	}
 }
 
-func dbImageUpdate(db *sql.DB, id int, fname string, sz int64, public bool, arch int, creationDate int64, expiryDate int64, properties map[string]string) error {
+func dbImageUpdate(db *sql.DB, id int, fname string, sz int64, public bool, arch int, creationDate time.Time, expiryDate time.Time, properties map[string]string) error {
 	tx, err := dbBegin(db)
 	if err != nil {
 		return err
@@ -264,7 +264,7 @@ func dbImageUpdate(db *sql.DB, id int, fname string, sz int64, public bool, arch
 	return nil
 }
 
-func dbImageInsert(db *sql.DB, fp string, fname string, sz int64, public bool, arch int, creationDate int64, expiryDate int64, properties map[string]string) error {
+func dbImageInsert(db *sql.DB, fp string, fname string, sz int64, public bool, arch int, creationDate time.Time, expiryDate time.Time, properties map[string]string) error {
 	tx, err := dbBegin(db)
 	if err != nil {
 		return err
diff --git a/lxd/db_test.go b/lxd/db_test.go
index a917a50..0f1c14a 100644
--- a/lxd/db_test.go
+++ b/lxd/db_test.go
@@ -4,6 +4,7 @@ import (
 	"database/sql"
 	"fmt"
 	"testing"
+	"time"
 
 	"github.com/lxc/lxd/shared"
 	"github.com/lxc/lxd/shared/logging"
@@ -417,16 +418,16 @@ func Test_dbImageGet_finds_image_for_fingerprint(t *testing.T) {
 		t.Fatal("Filename should be set.")
 	}
 
-	if result.CreationDate != 1431547174 {
-		t.Fatal(result.CreationDate)
+	if result.CreationDate != time.Unix(1431547174, 0).UTC() {
+		t.Fatal(fmt.Sprintf("%s != %s", result.CreationDate, time.Unix(1431547174, 0)))
 	}
 
-	if result.ExpiryDate != 1431547175 { // It was short lived
-		t.Fatal(result.ExpiryDate)
+	if result.ExpiryDate != time.Unix(1431547175, 0).UTC() { // It was short lived
+		t.Fatal(fmt.Sprintf("%s != %s", result.ExpiryDate, time.Unix(1431547175, 0)))
 	}
 
-	if result.UploadDate != 1431547176 {
-		t.Fatal(result.UploadDate)
+	if result.UploadDate != time.Unix(1431547176, 0).UTC() {
+		t.Fatal(fmt.Sprintf("%s != %s", result.UploadDate, time.Unix(1431547176, 0)))
 	}
 }
 
diff --git a/lxd/images.go b/lxd/images.go
index 197e833..a62bad5 100644
--- a/lxd/images.go
+++ b/lxd/images.go
@@ -16,6 +16,7 @@ import (
 	"strconv"
 	"strings"
 	"sync"
+	"time"
 
 	"github.com/gorilla/mux"
 	"gopkg.in/yaml.v2"
@@ -581,8 +582,8 @@ func getImgPostInfo(d *Daemon, r *http.Request,
 	}
 
 	info.Architecture, _ = shared.ArchitectureId(imageMeta.Architecture)
-	info.CreationDate = imageMeta.CreationDate
-	info.ExpiryDate = imageMeta.ExpiryDate
+	info.CreationDate = time.Unix(imageMeta.CreationDate, 0)
+	info.ExpiryDate = time.Unix(imageMeta.ExpiryDate, 0)
 
 	info.Properties = imageMeta.Properties
 	if len(propHeaders) > 0 {
@@ -751,14 +752,14 @@ func getImageMetadata(fname string) (*imageMetadata, error) {
 		return nil, fmt.Errorf("Could not extract image metadata %s from tar: %v (%s)", metadataName, err, outputLines[0])
 	}
 
-	metadata := new(imageMetadata)
+	metadata := imageMetadata{}
 	err = yaml.Unmarshal(output, &metadata)
 
 	if err != nil {
 		return nil, fmt.Errorf("Could not parse %s: %v", metadataName, err)
 	}
 
-	return metadata, nil
+	return &metadata, nil
 }
 
 func doImagesGet(d *Daemon, recursion bool, public bool) (interface{}, error) {
diff --git a/shared/container.go b/shared/container.go
index efa8274..9ff9d51 100644
--- a/shared/container.go
+++ b/shared/container.go
@@ -1,6 +1,8 @@
 package shared
 
-import ()
+import (
+	"time"
+)
 
 type Ip struct {
 	Interface string `json:"interface"`
@@ -23,15 +25,15 @@ type ContainerExecControl struct {
 }
 
 type SnapshotInfo struct {
-	CreationDate int64  `json:"creation_date"`
-	Name         string `json:"name"`
-	Stateful     bool   `json:"stateful"`
+	CreationDate time.Time `json:"created_at"`
+	Name         string    `json:"name"`
+	Stateful     bool      `json:"stateful"`
 }
 
 type ContainerInfo struct {
 	Architecture    int               `json:"architecture"`
 	Config          map[string]string `json:"config"`
-	CreationDate    int64             `json:"creation_date"`
+	CreationDate    time.Time         `json:"created_at"`
 	Devices         Devices           `json:"devices"`
 	Ephemeral       bool              `json:"ephemeral"`
 	ExpandedConfig  map[string]string `json:"expanded_config"`
diff --git a/shared/image.go b/shared/image.go
index 32199b5..9bebb21 100644
--- a/shared/image.go
+++ b/shared/image.go
@@ -1,5 +1,9 @@
 package shared
 
+import (
+	"time"
+)
+
 type ImageProperties map[string]string
 
 type ImageAlias struct {
@@ -18,9 +22,9 @@ type ImageInfo struct {
 	Properties   map[string]string `json:"properties"`
 	Public       bool              `json:"public"`
 	Size         int64             `json:"size"`
-	CreationDate int64             `json:"created_at"`
-	ExpiryDate   int64             `json:"expires_at"`
-	UploadDate   int64             `json:"uploaded_at"`
+	CreationDate time.Time         `json:"created_at"`
+	ExpiryDate   time.Time         `json:"expires_at"`
+	UploadDate   time.Time         `json:"uploaded_at"`
 }
 
 /*

From 629d1e53e895b4f8b23ceaa45fe3d17dd28358e3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 17 Feb 2016 15:39:17 -0500
Subject: [PATCH 10/17] Hide "target" field in alias section of images
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>
---
 client.go        |  6 +++---
 lxc/image.go     |  6 +++---
 lxd/db_images.go |  2 +-
 lxd/images.go    |  8 ++++----
 shared/image.go  | 11 ++++++++---
 5 files changed, 19 insertions(+), 14 deletions(-)

diff --git a/client.go b/client.go
index 6b498eb..a0cab2c 100644
--- a/client.go
+++ b/client.go
@@ -960,13 +960,13 @@ func (c *Client) DeleteAlias(alias string) error {
 	return err
 }
 
-func (c *Client) ListAliases() ([]shared.ImageAlias, error) {
+func (c *Client) ListAliases() (shared.ImageAliases, error) {
 	resp, err := c.get("images/aliases?recursion=1")
 	if err != nil {
 		return nil, err
 	}
 
-	var result []shared.ImageAlias
+	var result shared.ImageAliases
 
 	if err := json.Unmarshal(resp.Metadata, &result); err != nil {
 		return nil, err
@@ -1029,7 +1029,7 @@ func (c *Client) GetAlias(alias string) string {
 		return ""
 	}
 
-	var result shared.ImageAlias
+	var result shared.ImageAliasesEntry
 	if err := json.Unmarshal(resp.Metadata, &result); err != nil {
 		return ""
 	}
diff --git a/lxc/image.go b/lxc/image.go
index ade0bb2..e7f9f69 100644
--- a/lxc/image.go
+++ b/lxc/image.go
@@ -499,7 +499,7 @@ func (c *imageCmd) dereferenceAlias(d *lxd.Client, inName string) string {
 	return result
 }
 
-func (c *imageCmd) shortestAlias(list shared.ImageAliases) string {
+func (c *imageCmd) shortestAlias(list []shared.ImageAlias) string {
 	shortest := ""
 	for _, l := range list {
 		if shortest == "" {
@@ -566,7 +566,7 @@ func (c *imageCmd) showImages(images []shared.ImageInfo, filters []string) error
 	return nil
 }
 
-func (c *imageCmd) showAliases(aliases []shared.ImageAlias) error {
+func (c *imageCmd) showAliases(aliases shared.ImageAliases) error {
 	data := [][]string{}
 	for _, alias := range aliases {
 		data = append(data, []string{alias.Name, alias.Target[0:12], alias.Description})
@@ -690,7 +690,7 @@ func (c *imageCmd) imageShouldShow(filters []string, state *shared.ImageInfo) bo
 			}
 		} else {
 			for _, alias := range state.Aliases {
-				if strings.Contains(alias.Target, filter) {
+				if strings.Contains(alias.Name, filter) {
 					found = true
 					break
 				}
diff --git a/lxd/db_images.go b/lxd/db_images.go
index 91348c9..33c0e8f 100644
--- a/lxd/db_images.go
+++ b/lxd/db_images.go
@@ -124,7 +124,7 @@ func dbImageGet(db *sql.DB, fingerprint string, public bool, strictMatching bool
 		return -1, nil, err
 	}
 
-	aliases := shared.ImageAliases{}
+	aliases := []shared.ImageAlias{}
 	for _, r := range results {
 		name = r[0].(string)
 		desc = r[0].(string)
diff --git a/lxd/images.go b/lxd/images.go
index a62bad5..d92b1dd 100644
--- a/lxd/images.go
+++ b/lxd/images.go
@@ -990,7 +990,7 @@ func aliasesGet(d *Daemon, r *http.Request) Response {
 		return BadRequest(err)
 	}
 	responseStr := []string{}
-	responseMap := []shared.ImageAlias{}
+	responseMap := shared.ImageAliases{}
 	for _, res := range results {
 		name = res[0].(string)
 		if !recursion {
@@ -1024,7 +1024,7 @@ func aliasGet(d *Daemon, r *http.Request) Response {
 	return SyncResponse(true, alias)
 }
 
-func doAliasGet(d *Daemon, name string, isTrustedClient bool) (shared.ImageAlias, error) {
+func doAliasGet(d *Daemon, name string, isTrustedClient bool) (shared.ImageAliasesEntry, error) {
 	q := `SELECT images.fingerprint, images_aliases.description
 			 FROM images_aliases
 			 INNER JOIN images
@@ -1039,10 +1039,10 @@ func doAliasGet(d *Daemon, name string, isTrustedClient bool) (shared.ImageAlias
 	arg2 := []interface{}{&fingerprint, &description}
 	err := dbQueryRowScan(d.db, q, arg1, arg2)
 	if err != nil {
-		return shared.ImageAlias{}, err
+		return shared.ImageAliasesEntry{}, err
 	}
 
-	return shared.ImageAlias{Name: name, Target: fingerprint, Description: description}, nil
+	return shared.ImageAliasesEntry{Name: name, Target: fingerprint, Description: description}, nil
 }
 
 func aliasDelete(d *Daemon, r *http.Request) Response {
diff --git a/shared/image.go b/shared/image.go
index 9bebb21..78445be 100644
--- a/shared/image.go
+++ b/shared/image.go
@@ -6,16 +6,21 @@ import (
 
 type ImageProperties map[string]string
 
-type ImageAlias struct {
+type ImageAliasesEntry struct {
 	Name        string `json:"name"`
 	Description string `json:"description"`
 	Target      string `json:"target"`
 }
 
-type ImageAliases []ImageAlias
+type ImageAliases []ImageAliasesEntry
+
+type ImageAlias struct {
+	Name        string `json:"name"`
+	Description string `json:"description"`
+}
 
 type ImageInfo struct {
-	Aliases      ImageAliases      `json:"aliases"`
+	Aliases      []ImageAlias      `json:"aliases"`
 	Architecture int               `json:"architecture"`
 	Fingerprint  string            `json:"fingerprint"`
 	Filename     string            `json:"filename"`

From f36047287cc4374922655552e7df502482ba20e1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 17 Feb 2016 16:27:26 -0500
Subject: [PATCH 11/17] Implement PUT /1.0/images/aliases/NAME
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/containers_post.go |  4 +++-
 lxd/db_images.go       | 40 +++++++++++++++++++++---------------
 lxd/images.go          | 56 ++++++++++++++++++++++++++++++--------------------
 3 files changed, 60 insertions(+), 40 deletions(-)

diff --git a/lxd/containers_post.go b/lxd/containers_post.go
index c5dff5f..f736469 100644
--- a/lxd/containers_post.go
+++ b/lxd/containers_post.go
@@ -65,10 +65,12 @@ func createFromImage(d *Daemon, req *containerPostReq) Response {
 			}
 		} else {
 
-			hash, err = dbImageAliasGet(d.db, req.Source.Alias)
+			_, alias, err := dbImageAliasGet(d.db, req.Source.Alias, true)
 			if err != nil {
 				return InternalError(err)
 			}
+
+			hash = alias.Target
 		}
 	} else if req.Source.Fingerprint != "" {
 		hash = req.Source.Fingerprint
diff --git a/lxd/db_images.go b/lxd/db_images.go
index 33c0e8f..c055980 100644
--- a/lxd/db_images.go
+++ b/lxd/db_images.go
@@ -154,27 +154,27 @@ func dbImageDelete(db *sql.DB, id int) error {
 	return nil
 }
 
-// Get an image's fingerprint for a given alias name.
-func dbImageAliasGet(db *sql.DB, name string) (fingerprint string, err error) {
-	q := `
-        SELECT
-            fingerprint
-        FROM images AS i JOIN images_aliases AS a
-        ON a.image_id == i.id
-        WHERE name=?`
-
-	inargs := []interface{}{name}
-	outfmt := []interface{}{&fingerprint}
+func dbImageAliasGet(db *sql.DB, name string, isTrustedClient bool) (int, shared.ImageAliasesEntry, error) {
+	q := `SELECT images_aliases.id, images.fingerprint, images_aliases.description
+			 FROM images_aliases
+			 INNER JOIN images
+			 ON images_aliases.image_id=images.id
+			 WHERE images_aliases.name=?`
+	if !isTrustedClient {
+		q = q + ` AND images.public=1`
+	}
 
-	err = dbQueryRowScan(db, q, inargs, outfmt)
+	var fingerprint, description string
+	id := -1
 
-	if err == sql.ErrNoRows {
-		return "", NoSuchObjectError
-	}
+	arg1 := []interface{}{name}
+	arg2 := []interface{}{&id, &fingerprint, &description}
+	err := dbQueryRowScan(db, q, arg1, arg2)
 	if err != nil {
-		return "", err
+		return -1, shared.ImageAliasesEntry{}, err
 	}
-	return fingerprint, nil
+
+	return id, shared.ImageAliasesEntry{Name: name, Target: fingerprint, Description: description}, nil
 }
 
 func dbImageAliasDelete(db *sql.DB, name string) error {
@@ -189,6 +189,12 @@ func dbImageAliasAdd(db *sql.DB, name string, imageID int, desc string) error {
 	return err
 }
 
+func dbImageAliasUpdate(db *sql.DB, id int, imageID int, desc string) error {
+	stmt := `UPDATE images_aliases SET image_id=?, description=? WHERE id=?`
+	_, err := dbExec(db, stmt, imageID, desc, id)
+	return err
+}
+
 func dbImageLastAccessUpdate(db *sql.DB, fingerprint string) error {
 	stmt := `UPDATE images SET last_use_date=strftime("%s") WHERE fingerprint=?`
 	_, err := dbExec(db, stmt, fingerprint)
diff --git a/lxd/images.go b/lxd/images.go
index d92b1dd..238c53e 100644
--- a/lxd/images.go
+++ b/lxd/images.go
@@ -279,10 +279,12 @@ func imgPostRemoteInfo(d *Daemon, req imagePostReq, op *operation) error {
 				return err
 			}
 		} else {
-			hash, err = dbImageAliasGet(d.db, req.Source["alias"])
+			_, alias, err := dbImageAliasGet(d.db, req.Source["alias"], true)
 			if err != nil {
 				return err
 			}
+
+			hash = alias.Target
 		}
 	} else if req.Source["fingerprint"] != "" {
 		hash = req.Source["fingerprint"]
@@ -949,6 +951,11 @@ type aliasPostReq struct {
 	Target      string `json:"target"`
 }
 
+type aliasPutReq struct {
+	Description string `json:"description"`
+	Target      string `json:"target"`
+}
+
 func aliasesPost(d *Daemon, r *http.Request) Response {
 	req := aliasPostReq{}
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -960,7 +967,7 @@ func aliasesPost(d *Daemon, r *http.Request) Response {
 	}
 
 	// This is just to see if the alias name already exists.
-	_, err := dbImageAliasGet(d.db, req.Name)
+	_, _, err := dbImageAliasGet(d.db, req.Name, true)
 	if err == nil {
 		return Conflict
 	}
@@ -998,7 +1005,7 @@ func aliasesGet(d *Daemon, r *http.Request) Response {
 			responseStr = append(responseStr, url)
 
 		} else {
-			alias, err := doAliasGet(d, name, d.isTrustedClient(r))
+			_, alias, err := dbImageAliasGet(d.db, name, d.isTrustedClient(r))
 			if err != nil {
 				continue
 			}
@@ -1016,7 +1023,7 @@ func aliasesGet(d *Daemon, r *http.Request) Response {
 func aliasGet(d *Daemon, r *http.Request) Response {
 	name := mux.Vars(r)["name"]
 
-	alias, err := doAliasGet(d, name, d.isTrustedClient(r))
+	_, alias, err := dbImageAliasGet(d.db, name, d.isTrustedClient(r))
 	if err != nil {
 		return SmartError(err)
 	}
@@ -1024,35 +1031,40 @@ func aliasGet(d *Daemon, r *http.Request) Response {
 	return SyncResponse(true, alias)
 }
 
-func doAliasGet(d *Daemon, name string, isTrustedClient bool) (shared.ImageAliasesEntry, error) {
-	q := `SELECT images.fingerprint, images_aliases.description
-			 FROM images_aliases
-			 INNER JOIN images
-			 ON images_aliases.image_id=images.id
-			 WHERE images_aliases.name=?`
-	if !isTrustedClient {
-		q = q + ` AND images.public=1`
+func aliasDelete(d *Daemon, r *http.Request) Response {
+	name := mux.Vars(r)["name"]
+	_, _, err := dbImageAliasGet(d.db, name, true)
+	if err != nil {
+		return SmartError(err)
 	}
 
-	var fingerprint, description string
-	arg1 := []interface{}{name}
-	arg2 := []interface{}{&fingerprint, &description}
-	err := dbQueryRowScan(d.db, q, arg1, arg2)
+	err = dbImageAliasDelete(d.db, name)
 	if err != nil {
-		return shared.ImageAliasesEntry{}, err
+		return SmartError(err)
 	}
 
-	return shared.ImageAliasesEntry{Name: name, Target: fingerprint, Description: description}, nil
+	return EmptySyncResponse
 }
 
-func aliasDelete(d *Daemon, r *http.Request) Response {
+func aliasPut(d *Daemon, r *http.Request) Response {
 	name := mux.Vars(r)["name"]
-	_, err := doAliasGet(d, name, true)
+
+	req := aliasPutReq{}
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		return BadRequest(err)
+	}
+
+	id, _, err := dbImageAliasGet(d.db, name, true)
 	if err != nil {
 		return SmartError(err)
 	}
 
-	err = dbImageAliasDelete(d.db, name)
+	imageId, _, err := dbImageGet(d.db, req.Target, false, false)
+	if err != nil {
+		return SmartError(err)
+	}
+
+	err = dbImageAliasUpdate(d.db, id, imageId, req.Description)
 	if err != nil {
 		return SmartError(err)
 	}
@@ -1140,4 +1152,4 @@ var imagesSecretCmd = Command{name: "images/{fingerprint}/secret", post: imageSe
 
 var aliasesCmd = Command{name: "images/aliases", post: aliasesPost, get: aliasesGet}
 
-var aliasCmd = Command{name: "images/aliases/{name:.*}", untrustedGet: true, get: aliasGet, delete: aliasDelete}
+var aliasCmd = Command{name: "images/aliases/{name:.*}", untrustedGet: true, get: aliasGet, delete: aliasDelete, put: aliasPut}

From 266ede6ed3514e474a7b16177ff08698d3412296 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 17 Feb 2016 16:42:44 -0500
Subject: [PATCH 12/17] Implement POST /1.0/images/aliases/NAME
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/db_images.go |  5 +++++
 lxd/images.go    | 23 ++++++++++++++++++++++-
 2 files changed, 27 insertions(+), 1 deletion(-)

diff --git a/lxd/db_images.go b/lxd/db_images.go
index c055980..bbed11a 100644
--- a/lxd/db_images.go
+++ b/lxd/db_images.go
@@ -177,6 +177,11 @@ func dbImageAliasGet(db *sql.DB, name string, isTrustedClient bool) (int, shared
 	return id, shared.ImageAliasesEntry{Name: name, Target: fingerprint, Description: description}, nil
 }
 
+func dbImageAliasRename(db *sql.DB, id int, name string) error {
+	_, err := dbExec(db, "UPDATE images_aliases SET name=? WHERE id=?", name, id)
+	return err
+}
+
 func dbImageAliasDelete(db *sql.DB, name string) error {
 	_, err := dbExec(db, "DELETE FROM images_aliases WHERE name=?", name)
 	return err
diff --git a/lxd/images.go b/lxd/images.go
index 238c53e..bc954db 100644
--- a/lxd/images.go
+++ b/lxd/images.go
@@ -1072,6 +1072,27 @@ func aliasPut(d *Daemon, r *http.Request) Response {
 	return EmptySyncResponse
 }
 
+func aliasPost(d *Daemon, r *http.Request) Response {
+	name := mux.Vars(r)["name"]
+
+	req := aliasPostReq{}
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		return BadRequest(err)
+	}
+
+	id, _, err := dbImageAliasGet(d.db, name, true)
+	if err != nil {
+		return SmartError(err)
+	}
+
+	err = dbImageAliasRename(d.db, id, req.Name)
+	if err != nil {
+		return SmartError(err)
+	}
+
+	return EmptySyncResponse
+}
+
 func imageExport(d *Daemon, r *http.Request) Response {
 	fingerprint := mux.Vars(r)["fingerprint"]
 
@@ -1152,4 +1173,4 @@ var imagesSecretCmd = Command{name: "images/{fingerprint}/secret", post: imageSe
 
 var aliasesCmd = Command{name: "images/aliases", post: aliasesPost, get: aliasesGet}
 
-var aliasCmd = Command{name: "images/aliases/{name:.*}", untrustedGet: true, get: aliasGet, delete: aliasDelete, put: aliasPut}
+var aliasCmd = Command{name: "images/aliases/{name:.*}", untrustedGet: true, get: aliasGet, delete: aliasDelete, put: aliasPut, post: aliasPost}

From 614f88d712f55ce0cbef84b5469edb4584763677 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 17 Feb 2016 17:03:40 -0500
Subject: [PATCH 13/17] Make /1.0/networks follow the specification
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/networks.go | 85 +++++++++++++++++++++++++++------------------------------
 1 file changed, 40 insertions(+), 45 deletions(-)

diff --git a/lxd/networks.go b/lxd/networks.go
index a670003..f649b4e 100644
--- a/lxd/networks.go
+++ b/lxd/networks.go
@@ -4,12 +4,9 @@ import (
 	"fmt"
 	"net"
 	"net/http"
-	"os"
-	"path"
 	"strconv"
 
 	"github.com/gorilla/mux"
-	"gopkg.in/lxc/go-lxc.v2"
 
 	"github.com/lxc/lxd/shared"
 )
@@ -51,43 +48,30 @@ func networksGet(d *Daemon, r *http.Request) Response {
 var networksCmd = Command{name: "networks", get: networksGet}
 
 type network struct {
-	Name    string   `json:"name"`
-	Type    string   `json:"type"`
-	Members []string `json:"members"`
+	Name   string   `json:"name"`
+	Type   string   `json:"type"`
+	UsedBy []string `json:"used_by"`
 }
 
-func children(iface string) []string {
-	p := path.Join("/sys/class/net", iface, "brif")
-
-	ret, _ := shared.ReadDir(p)
-
-	return ret
-}
-
-func isBridge(iface *net.Interface) bool {
-	p := path.Join("/sys/class/net", iface.Name, "bridge")
-	stat, err := os.Stat(p)
-	if err != nil {
-		return false
-	}
+func isOnBridge(c container, bridge string) bool {
+	for _, device := range c.ExpandedDevices() {
+		if device["type"] != "nic" {
+			continue
+		}
 
-	return stat.IsDir()
-}
+		if !shared.StringInSlice(device["nictype"], []string{"bridged", "macvlan"}) {
+			continue
+		}
 
-func isOnBridge(c *lxc.Container, bridge string) bool {
-	kids := children(bridge)
-	for i := 0; i < len(c.ConfigItem("lxc.network")); i++ {
-		interfaceType := c.RunningConfigItem(fmt.Sprintf("lxc.network.%d.type", i))
-		if interfaceType[0] == "veth" {
-			cif := c.RunningConfigItem(fmt.Sprintf("lxc.network.%d.veth.pair", i))[0]
-			for _, kif := range kids {
-				if cif == kif {
-					return true
-				}
+		if device["parent"] == "" {
+			continue
+		}
 
-			}
+		if device["parent"] == bridge {
+			return true
 		}
 	}
+
 	return false
 }
 
@@ -108,24 +92,35 @@ func doNetworkGet(d *Daemon, name string) (network, error) {
 		return network{}, err
 	}
 
+	// Prepare the response
 	n := network{}
 	n.Name = iface.Name
-	n.Members = make([]string, 0)
+	n.UsedBy = []string{}
+
+	// Look for containers using the interface
+	cts, err := dbContainersList(d.db, cTypeRegular)
+	if err != nil {
+		return network{}, err
+	}
 
+	for _, ct := range cts {
+		c, err := containerLoadByName(d, ct)
+		if err != nil {
+			return network{}, err
+		}
+
+		if isOnBridge(c, n.Name) {
+			n.UsedBy = append(n.UsedBy, fmt.Sprintf("/%s/containers/%s", shared.APIVersion, ct))
+		}
+	}
+
+	// Set the device type as needed
 	if shared.IsLoopback(iface) {
 		n.Type = "loopback"
-	} else if isBridge(iface) {
+	} else if shared.PathExists(fmt.Sprintf("/sys/class/net/%s/bridge", n.Name)) {
 		n.Type = "bridge"
-		for _, ct := range lxc.ActiveContainerNames(d.lxcpath) {
-			c, err := containerLoadByName(d, ct)
-			if err != nil {
-				return network{}, err
-			}
-
-			if isOnBridge(c.LXContainerGet(), n.Name) {
-				n.Members = append(n.Members, ct)
-			}
-		}
+	} else if shared.PathExists(fmt.Sprintf("/sys/class/net/%s/device", n.Name)) {
+		n.Type = "physical"
 	} else {
 		n.Type = "unknown"
 	}

From 1af542bedd33421a386bb0d0ad9e5748e0e8fe6d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 17 Feb 2016 17:08:05 -0500
Subject: [PATCH 14/17] Fix operation wait timeout
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/operations.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lxd/operations.go b/lxd/operations.go
index 7b0d562..639bee3 100644
--- a/lxd/operations.go
+++ b/lxd/operations.go
@@ -302,7 +302,7 @@ func (op *operation) WaitFinal(timeout int) (bool, error) {
 				return false, nil
 
 			case <-timer.C:
-				break
+				return false, nil
 			}
 		}
 	}

From c2bcd44165a662f44d3d4f5015182e6b6f7b32d1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 17 Feb 2016 18:10:59 -0500
Subject: [PATCH 15/17] Fix GET /1.0/certificates
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/certificates.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lxd/certificates.go b/lxd/certificates.go
index 7451998..e432f4a 100644
--- a/lxd/certificates.go
+++ b/lxd/certificates.go
@@ -44,7 +44,7 @@ func certificatesGet(d *Daemon, r *http.Request) Response {
 
 	body := []string{}
 	for _, cert := range d.clientCerts {
-		fingerprint := certGenerateFingerprint(&cert)
+		fingerprint := fmt.Sprintf("/%s/certificates/%s", shared.APIVersion, certGenerateFingerprint(&cert))
 		body = append(body, fingerprint)
 	}
 

From 447993ce7bac35ac6f05a758d5233974d77355f2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 17 Feb 2016 18:11:38 -0500
Subject: [PATCH 16/17] Implement POST /1.0/profiles/NAME
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/db_profiles.go | 17 +++++++++++++++++
 lxd/profiles.go    | 24 +++++++++++++++++++++++-
 2 files changed, 40 insertions(+), 1 deletion(-)

diff --git a/lxd/db_profiles.go b/lxd/db_profiles.go
index ee0f986..78cfbad 100644
--- a/lxd/db_profiles.go
+++ b/lxd/db_profiles.go
@@ -171,6 +171,23 @@ func dbProfileDelete(db *sql.DB, name string) error {
 	return err
 }
 
+func dbProfileUpdate(db *sql.DB, name string, newName string) error {
+	tx, err := dbBegin(db)
+	if err != nil {
+		return err
+	}
+
+	_, err = tx.Exec("UPDATE profiles SET name=? WHERE name=?", newName, name)
+	if err != nil {
+		tx.Rollback()
+		return err
+	}
+
+	err = txCommit(tx)
+
+	return err
+}
+
 func dbProfileConfigClear(tx *sql.Tx, id int64) error {
 	_, err := tx.Exec("DELETE FROM profiles_config WHERE profile_id=?", id)
 	if err != nil {
diff --git a/lxd/profiles.go b/lxd/profiles.go
index 8197d95..b37c811 100644
--- a/lxd/profiles.go
+++ b/lxd/profiles.go
@@ -227,6 +227,28 @@ func profilePut(d *Daemon, r *http.Request) Response {
 	return EmptySyncResponse
 }
 
+// The handler for the post operation.
+func profilePost(d *Daemon, r *http.Request) Response {
+	name := mux.Vars(r)["name"]
+
+	req := profilesPostReq{}
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		return BadRequest(err)
+	}
+
+	// Sanity checks
+	if req.Name == "" {
+		return BadRequest(fmt.Errorf("No name provided"))
+	}
+
+	err := dbProfileUpdate(d.db, name, req.Name)
+	if err != nil {
+		return InternalError(err)
+	}
+
+	return EmptySyncResponse
+}
+
 // The handler for the delete operation.
 func profileDelete(d *Daemon, r *http.Request) Response {
 	name := mux.Vars(r)["name"]
@@ -239,4 +261,4 @@ func profileDelete(d *Daemon, r *http.Request) Response {
 	return EmptySyncResponse
 }
 
-var profileCmd = Command{name: "profiles/{name}", get: profileGet, put: profilePut, delete: profileDelete}
+var profileCmd = Command{name: "profiles/{name}", get: profileGet, put: profilePut, delete: profileDelete, post: profilePost}

From a118899f8ca0dffab16d49cb7accee6c3bad0fd0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 17 Feb 2016 18:11:53 -0500
Subject: [PATCH 17/17] specs: Update rest-api to match 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/rest-api.md | 826 +++++++++++++++++++++++++++++++++---------------------
 1 file changed, 509 insertions(+), 317 deletions(-)

diff --git a/specs/rest-api.md b/specs/rest-api.md
index 05e6583..06bf957 100644
--- a/specs/rest-api.md
+++ b/specs/rest-api.md
@@ -31,10 +31,10 @@ There are three standard return types:
 For a standard synchronous operation, the following dict is returned:
 
     {
-        'type': "sync",
-        'status': "Success",
-        'status_code': 200,
-        'metadata': {}                          # Extra resource/action specific metadata
+        "type": "sync",
+        "status": "Success",
+        "status_code": 200,
+        "metadata": {}                          # Extra resource/action specific metadata
     }
 
 HTTP code must be 200.
@@ -46,11 +46,11 @@ and the Location HTTP header is set to the operation URL.
 The body is a dict with the following structure:
 
     {
-        'type': "async",
-        'status': "OK",
-        'status_code': 100,
-        'operation': "/1.0/containers/<id>",                    # URL to the background operation
-        'metadata': {}                                          # Operation metadata (see below)
+        "type": "async",
+        "status": "OK",
+        "status_code": 100,
+        "operation": "/1.0/containers/<id>",                    # URL to the background operation
+        "metadata": {}                                          # Operation metadata (see below)
     }
 
 The operation metadata structure looks like:
@@ -86,10 +86,10 @@ There are various situations in which something may immediately go
 wrong, in those cases, the following return value is used:
 
     {
-        'type': "error",
-        'error': "Failure",
-        'error_code': 400,
-        'metadata': {}                      # More details about the error
+        "type": "error",
+        "error": "Failure",
+        "error_code": 400,
+        "metadata": {}                      # More details about the error
     }
 
 HTTP code must be one of of 400, 401, 403, 404, 409, 412 or 500.
@@ -162,6 +162,9 @@ The default value is 0 which means that collection member URLs are
 returned. Setting it to 1 will have those URLs be replaced by the object
 they point to (typically a dict).
 
+Recursion is implemented by simply replacing any pointer to an job (URL)
+by the object itself.
+
 # API structure
  * /
    * /1.0
@@ -197,7 +200,13 @@ they point to (typically a dict).
  * Description: List of supported APIs
  * Authentication: guest
  * Operation: sync
- * Return: list of supported API endpoint URLs (by default ['/1.0'])
+ * Return: list of supported API endpoint URLs
+
+Return value:
+
+    [
+        "/1.0"
+    ]
 
 ## /1.0/
 ### GET
@@ -209,31 +218,42 @@ they point to (typically a dict).
 Return value (if trusted):
 
     {
-        'auth': "trusted",                              # Authentication state, one of "guest", "untrusted" or "trusted"
-        'public': true,                                 # Whether the server should be treated as a public (read-only) remote by the client
-        'api_compat': 0,                                # Used to determine API functionality
-        'config': {"trust_password": true},             # Host configuration
-        'environment': {                                # Various information about the host (OS, kernel, ...)
-                        'addresses': ["1.2.3.4:8443", "[1234::1234]:8443"],
-                        'architectures': [1, 2],
-                        'certificate': 'PEM certificate',
-                        'driver': "lxc",
-                        'driver_version': "1.0.6",
-                        'kernel': "Linux",
-                        'kernel_architecture': "x86_64",
-                        'kernel_version': "3.16",
-                        'storage': "btrfs",
-                        'storage_version': "3.19",
-                        'server': "lxd",
-                        'server_pid': 10224,
-                        'server_version': "0.8.1"}
+        "api_compat": 1,                                # Used to determine API functionality
+        "auth": "trusted",                              # Authentication state, one of "guest", "untrusted" or "trusted"
+        "config": {                                     # Host configuration
+            "core.trust_password": true,
+            "core.https_address": "[::]:8443"
+        },
+        "environment": {                                # Various information about the host (OS, kernel, ...)
+            "addresses": [
+                "1.2.3.4:8443",
+                "[1234::1234]:8443"
+            ],
+            "architectures": [
+                2,
+                1
+            ],
+            "certificate": "PEM certificate",
+            "driver": "lxc",
+            "driver_version": "1.0.6",
+            "kernel": "Linux",
+            "kernel_architecture": "x86_64",
+            "kernel_version": "3.16",
+            "server": "lxd",
+            "server_pid": 10224,
+            "server_version": "0.8.1"}
+            "storage": "btrfs",
+            "storage_version": "3.19",
+        },
+        "public": false,                                # Whether the server should be treated as a public (read-only) remote by the client
     }
 
 Return value (if guest or untrusted):
 
     {
-        'auth': "guest",                        # Authentication state, one of "guest", "untrusted" or "trusted"
-        'api_compat': 0,                        # Used to determine API functionality
+        "api_compat": 1,                        # Used to determine API functionality
+        "auth": "guest",                        # Authentication state, one of "guest", "untrusted" or "trusted"
+        "public": false,                        # Whether the server should be treated as a public (read-only) remote by the client
     }
 
 ### PUT
@@ -242,12 +262,85 @@ Return value (if guest or untrusted):
  * Operation: sync
  * Return: standard return value or standard error
 
+Input (replaces any existing config with the provided one):
+
+    {
+        "config": {
+            "core.trust_password": "my-new-password",
+            "storage.zfs_pool_name": "lxd"
+        }
+    }
+
+## /1.0/certificates
+### GET
+ * Description: list of trusted certificates
+ * Authentication: trusted
+ * Operation: sync
+ * Return: list of URLs for trusted certificates
+
+Return:
+
+    [
+        "/1.0/certificates/3ee64be3c3c7d617a7470e14f2d847081ad467c8c26e1caad841c8f67f7c7b09"
+    ]
+
+### POST
+ * Description: add a new trusted certificate
+ * Authentication: trusted or untrusted
+ * Operation: sync
+ * Return: standard return value or standard error
+
 Input:
 
     {
-        'config': {"trust_password": "my-new-password"}
+        "type": "client",                       # Certificate type (keyring), currently only client
+        "certificate": "BASE64",                # If provided, a valid x509 certificate. If not, the client certificate of the connection will be used
+        "name": "foo"                           # An optional name for the certificate. If nothing is provided, the host in the TLS header for the request is used.
+        "password": "server-trust-password"     # The trust password for that server (only required if untrusted)
+    }
+
+## /1.0/certificates/\<fingerprint\>
+### GET
+ * Description: trusted certificate information
+ * Authentication: trusted
+ * Operation: sync
+ * Return: dict representing a trusted certificate
+
+Output:
+
+    {
+        "type": "client",
+        "certificate": "PEM certificate"
+        "fingerprint": "SHA256 Hash of the raw certificate"
     }
 
+### DELETE
+ * Description: Remove a trusted certificate
+ * Authentication: trusted
+ * Operation: sync
+ * Return: standard return value or standard error
+
+Input (none at present):
+
+    {
+    }
+
+HTTP code for this should be 202 (Accepted).
+
+# Async operations
+Any operation which may take more than a second to be done must be done
+in the background, returning a background operation ID to the client.
+
+The client will then be able to either poll for a status update or wait
+for a notification using the long-poll API.
+
+# Notifications
+A long-poll API is available for notifications, different notification
+types exist to limit the traffic going to the client.
+
+It's recommend 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.
 ## /1.0/containers
 ### GET
  * Description: List of containers
@@ -255,6 +348,13 @@ Input:
  * Operation: sync
  * Return: list of URLs for containers this server publishes
 
+Return value:
+
+    [
+        "/1.0/containers/blah",
+        "/1.0/containers/blah1"
+    ]
+
 ### POST
  * Description: Create a new container
  * Authentication: trusted
@@ -264,114 +364,114 @@ Input:
 Input (container based on a local image with the "ubuntu/devel" alias):
 
     {
-        'name': "my-new-container",                                         # 64 chars max, ASCII, no slash, no colon and no comma
-        'architecture': 2,
-        'profiles': ["default"],                                            # List of profiles
-        'ephemeral': true,                                                  # Whether to destroy the container on shutdown
-        'config': {'limits.cpu': "2"},                                      # Config override.
-        'source': {'type': "image",                                         # Can be: "image", "migration", "copy" or "none"
-                   'alias': "ubuntu/devel"},                                # Name of the alias
+        "name": "my-new-container",                                         # 64 chars max, ASCII, no slash, no colon and no comma
+        "architecture": 2,
+        "profiles": ["default"],                                            # List of profiles
+        "ephemeral": true,                                                  # Whether to destroy the container on shutdown
+        "config": {"limits.cpu": "2"},                                      # Config override.
+        "source": {"type": "image",                                         # Can be: "image", "migration", "copy" or "none"
+                   "alias": "ubuntu/devel"},                                # Name of the alias
     }
 
 Input (container based on a local image identified by its fingerprint):
 
     {
-        'name': "my-new-container",                                         # 64 chars max, ASCII, no slash, no colon and no comma
-        'architecture': 2,
-        'profiles': ["default"],                                            # List of profiles
-        'ephemeral': true,                                                  # Whether to destroy the container on shutdown
-        'config': {'limits.cpu': "2"},                                      # Config override.
-        'source': {'type': "image",                                         # Can be: "image", "migration", "copy" or "none"
-                   'fingerprint': "SHA-256"},                               # Fingerprint
+        "name": "my-new-container",                                         # 64 chars max, ASCII, no slash, no colon and no comma
+        "architecture": 2,
+        "profiles": ["default"],                                            # List of profiles
+        "ephemeral": true,                                                  # Whether to destroy the container on shutdown
+        "config": {"limits.cpu": "2"},                                      # Config override.
+        "source": {"type": "image",                                         # Can be: "image", "migration", "copy" or "none"
+                   "fingerprint": "SHA-256"},                               # Fingerprint
     }
 
 Input (container based on most recent match based on image properties):
 
     {
-        'name': "my-new-container",                                         # 64 chars max, ASCII, no slash, no colon and no comma
-        'architecture': 2,
-        'profiles': ["default"],                                            # List of profiles
-        'ephemeral': true,                                                  # Whether to destroy the container on shutdown
-        'config': {'limits.cpu': "2"},                                      # Config override.
-        'source': {'type': "image",                                         # Can be: "image", "migration", "copy" or "none"
-                   'properties': {                                          # Properties
-                        'os': "ubuntu",
-                        'release': "14.04",
-                        'architecture': "x86_64"
+        "name": "my-new-container",                                         # 64 chars max, ASCII, no slash, no colon and no comma
+        "architecture": 2,
+        "profiles": ["default"],                                            # List of profiles
+        "ephemeral": true,                                                  # Whether to destroy the container on shutdown
+        "config": {"limits.cpu": "2"},                                      # Config override.
+        "source": {"type": "image",                                         # Can be: "image", "migration", "copy" or "none"
+                   "properties": {                                          # Properties
+                        "os": "ubuntu",
+                        "release": "14.04",
+                        "architecture": "x86_64"
                     }},
     }
 
 Input (container without a pre-populated rootfs, useful when attaching to an existing one):
 
     {
-        'name': "my-new-container",                                         # 64 chars max, ASCII, no slash, no colon and no comma
-        'architecture': 2,
-        'profiles': ["default"],                                            # List of profiles
-        'ephemeral': true,                                                  # Whether to destroy the container on shutdown
-        'config': {'limits.cpu': "2"},                                      # Config override.
-        'source': {'type': "none"},                                         # Can be: "image", "migration", "copy" or "none"
+        "name": "my-new-container",                                         # 64 chars max, ASCII, no slash, no colon and no comma
+        "architecture": 2,
+        "profiles": ["default"],                                            # List of profiles
+        "ephemeral": true,                                                  # Whether to destroy the container on shutdown
+        "config": {"limits.cpu": "2"},                                      # Config override.
+        "source": {"type": "none"},                                         # Can be: "image", "migration", "copy" or "none"
     }
 
 Input (using a public remote image):
 
     {
-        'name': "my-new-container",                                         # 64 chars max, ASCII, no slash, no colon and no comma
-        'architecture': 2,
-        'profiles': ["default"],                                            # List of profiles
-        'ephemeral': true,                                                  # Whether to destroy the container on shutdown
-        'config': {'limits.cpu': "2"},                                      # Config override.
-        'source': {'type': "image",                                         # Can be: "image", "migration", "copy" or "none"
-                   'mode': "pull",                                          # One of "local" (default), "pull" or "receive"
-                   'server': "https://10.0.2.3:8443",                       # Remote server (pull mode only)
-                   'certificate': "PEM certificate",                        # Optional PEM certificate. If not mentioned, system CA is used.
-                   'alias': "ubuntu/devel"},                                # Name of the alias
+        "name": "my-new-container",                                         # 64 chars max, ASCII, no slash, no colon and no comma
+        "architecture": 2,
+        "profiles": ["default"],                                            # List of profiles
+        "ephemeral": true,                                                  # Whether to destroy the container on shutdown
+        "config": {"limits.cpu": "2"},                                      # Config override.
+        "source": {"type": "image",                                         # Can be: "image", "migration", "copy" or "none"
+                   "mode": "pull",                                          # One of "local" (default) or "pull"
+                   "server": "https://10.0.2.3:8443",                       # Remote server (pull mode only)
+                   "certificate": "PEM certificate",                        # Optional PEM certificate. If not mentioned, system CA is used.
+                   "alias": "ubuntu/devel"},                                # Name of the alias
     }
 
 
 Input (using a private remote image after having obtained a secret for that image):
 
     {
-        'name': "my-new-container",                                         # 64 chars max, ASCII, no slash, no colon and no comma
-        'architecture': 2,
-        'profiles': ["default"],                                            # List of profiles
-        'ephemeral': true,                                                  # Whether to destroy the container on shutdown
-        'config': {'limits.cpu': "2"},                                      # Config override.
-        'source': {'type': "image",                                         # Can be: "image", "migration", "copy" or "none"
-                   'mode': "pull",                                          # One of "local" (default), "pull" or "receive"
-                   'server': "https://10.0.2.3:8443",                       # Remote server (pull mode only)
-                   'secret': "my-secret-string",                            # Secret to use to retrieve the image (pull mode only)
-                   'certificate': "PEM certificate",                        # Optional PEM certificate. If not mentioned, system CA is used.
-                   'alias': "ubuntu/devel"},                                # Name of the alias
+        "name": "my-new-container",                                         # 64 chars max, ASCII, no slash, no colon and no comma
+        "architecture": 2,
+        "profiles": ["default"],                                            # List of profiles
+        "ephemeral": true,                                                  # Whether to destroy the container on shutdown
+        "config": {"limits.cpu": "2"},                                      # Config override.
+        "source": {"type": "image",                                         # Can be: "image", "migration", "copy" or "none"
+                   "mode": "pull",                                          # One of "local" (default) or "pull"
+                   "server": "https://10.0.2.3:8443",                       # Remote server (pull mode only)
+                   "secret": "my-secret-string",                            # Secret to use to retrieve the image (pull mode only)
+                   "certificate": "PEM certificate",                        # Optional PEM certificate. If not mentioned, system CA is used.
+                   "alias": "ubuntu/devel"},                                # Name of the alias
     }
 
 Input (using a remote container, sent over the migration websocket):
 
     {
-        'name': "my-new-container",                                                     # 64 chars max, ASCII, no slash, no colon and no comma
-        'architecture': 2,
-        'profiles': ["default"],                                                        # List of profiles
-        'ephemeral': true,                                                              # Whether to destroy the container on shutdown
-        'config': {'limits.cpu': "2"},                                                  # Config override.
-        'source': {'type': "migration",                                                 # Can be: "image", "migration", "copy" or "none"
-                   'mode': "pull",                                                      # One of "pull" or "receive"
-                   'operation': "https://10.0.2.3:8443/1.0/operations/<UUID>",          # Full URL to the remote operation (pull mode only)
-                   'certificate': "PEM certificate",                        # Optional PEM certificate. If not mentioned, system CA is used.
-                   'base-image': "<some hash>"                                          # Optional, the base image the container was created from
-                   'secrets': {'control': "my-secret-string",                           # Secrets to use when talking to the migration source
-                               'criu':    "my-other-secret",
-                               'fs':      "my third secret"},
+        "name": "my-new-container",                                                     # 64 chars max, ASCII, no slash, no colon and no comma
+        "architecture": 2,
+        "profiles": ["default"],                                                        # List of profiles
+        "ephemeral": true,                                                              # Whether to destroy the container on shutdown
+        "config": {"limits.cpu": "2"},                                                  # Config override.
+        "source": {"type": "migration",                                                 # Can be: "image", "migration", "copy" or "none"
+                   "mode": "pull",                                                      # Only "pull" is supported for now
+                   "operation": "https://10.0.2.3:8443/1.0/operations/<UUID>",          # Full URL to the remote operation (pull mode only)
+                   "certificate": "PEM certificate",                        # Optional PEM certificate. If not mentioned, system CA is used.
+                   "base-image": "<some hash>"                                          # Optional, the base image the container was created from
+                   "secrets": {"control": "my-secret-string",                           # Secrets to use when talking to the migration source
+                               "criu":    "my-other-secret",
+                               "fs":      "my third secret"},
     }
 
 Input (using a local container):
 
     {
-        'name': "my-new-container",                                                     # 64 chars max, ASCII, no slash, no colon and no comma
-        'architecture': 2,
-        'profiles': ["default"],                                                        # List of profiles
-        'ephemeral': true,                                                              # Whether to destroy the container on shutdown
-        'config': {'limits.cpu': "2"},                                                  # Config override.
-        'source': {'type': "copy",                                                      # Can be: "image", "migration", "copy" or "none"
-                   'source': "my-old-container"}                                        # Name of the source container
+        "name": "my-new-container",                                                     # 64 chars max, ASCII, no slash, no colon and no comma
+        "architecture": 2,
+        "profiles": ["default"],                                                        # List of profiles
+        "ephemeral": true,                                                              # Whether to destroy the container on shutdown
+        "config": {"limits.cpu": "2"},                                                  # Config override.
+        "source": {"type": "copy",                                                      # Can be: "image", "migration", "copy" or "none"
+                   "source": "my-old-container"}                                        # Name of the source container
     }
 
 
@@ -385,45 +485,43 @@ Input (using a local container):
 Output:
 
     {
-        'name': "my-container",
-        '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': {
-                'type': "disk",
-                'path': "/",
-                'source': "UUID=8f7fdf5e-dc60-4524-b9fe-634f82ac2fb6"
+        "architecture": 2,
+        "config": {
+            "limits.cpu": "3",
+            "volatile.base_image": "97d97a3d1d053840ca19c86cdd0596cf1be060c5157d31407f2a4f9f350c78cc",
+            "volatile.eth0.hwaddr": "00:16:3e:1c:94:38"
+        },
+        "created_at": "2016-02-16T01:05:05Z",
+        "devices": {
+            "rootfs": {
+                "path": "/",
+                "type": "disk"
             }
         },
-        'expanded_devices': {  # the result of expanding profiles and adding the container's local devices
-            'rootfs': {
-                'type': "disk",
-                'path': "/",
-                'source': "UUID=8f7fdf5e-dc60-4524-b9fe-634f82ac2fb6"}
-            },
+        "ephemeral": false,
+        "expanded_config": {    # the result of expanding profiles and adding the container's local config
+            "limits.cpu": "3",
+            "volatile.base_image": "97d97a3d1d053840ca19c86cdd0596cf1be060c5157d31407f2a4f9f350c78cc",
+            "volatile.eth0.hwaddr": "00:16:3e:1c:94:38"
+        },
+        "expanded_devices": {   # the result of expanding profiles and adding the container's local devices
             "eth0": {
-                "type": "nic"
-                "parent": "lxcbr0",
-                "hwaddr": "00:16:3e:f4:e7:1c",
                 "name": "eth0",
                 "nictype": "bridged",
+                "parent": "lxcbr0",
+                "type": "nic"
+            },
+            "root": {
+                "path": "/",
+                "type": "disk"
             }
         },
-        'status': {
-                    'status': "Running",
-                    'status_code': 103,
-                    'ips': [{'interface': "eth0",
-                             'protocol': "INET6",
-                             'address': "2001:470:b368:1020:1::2",
-                             'host_veth': "vethGMDIY9"},
-                            {'interface': "eth0",
-                             'protocol': "INET",
-                             'address': "172.16.15.30",
-                             'host_veth': "vethGMDIY9"}]},
+        "name": "my-container",
+        "profiles": [
+            "default"
+        ],
+        "status": "Running",
+        "status_code": 103
     }
 
 
@@ -435,6 +533,26 @@ Output:
 
 Input (update container configuration):
 
+    {
+        "architecture": 2,
+        "config": {
+            "limits.cpu": "4",
+            "volatile.base_image": "97d97a3d1d053840ca19c86cdd0596cf1be060c5157d31407f2a4f9f350c78cc",
+            "volatile.eth0.hwaddr": "00:16:3e:1c:94:38"
+        },
+        "devices": {
+            "rootfs": {
+                "path": "/",
+                "type": "disk"
+            }
+        },
+        "ephemeral": true,
+        "profiles": [
+            "default"
+        ]
+    }
+
+
 Takes the same structure as that returned by GET but doesn't allow name
 changes (see POST below) or changes to the status sub-dict (since that's
 read-only).
@@ -442,7 +560,7 @@ read-only).
 Input (restore snapshot):
 
     {
-        'restore': "snapshot-name"
+        "restore": "snapshot-name"
     }
 
 ### POST
@@ -456,12 +574,12 @@ Renaming to an existing name must return the 409 (Conflict) HTTP code.
 Input (simple rename):
 
     {
-        'name': "new-name"
+        "name": "new-name"
     }
 
 Input (migration across lxd instances):
     {
-        "migration": true,
+        "migration": true
     }
 
 The migration does not actually start until someone (i.e. another lxd instance)
@@ -470,9 +588,9 @@ connects to all the websockets and begins negotiation with the source.
 Output in metadata section (for migration):
 
     {
-        "control": "secret1",
-        "criu": "secret2",
-        "fs": "secret3",
+        "control": "secret1",       # Migration control socket
+        "criu": "secret2",          # State transfer socket (only if live migrating)
+        "fs": "secret3"             # Filesystem transfer socket
     }
 
 These are the secrets that should be passed to the create call.
@@ -498,8 +616,36 @@ HTTP code for this should be 202 (Accepted).
  * Return: dict representing current state
 
     {
-        'status': "Running",
-        'status_code': 103
+        "status": "Running",
+        "status_code": 103,
+        "init": 16126,
+        "processcount": 7,
+        "ips": [
+            {
+                "interface": "eth0",
+                "protocol": "IPV4",
+                "address": "172.17.0.242",
+                "host_veth": ""
+            },
+            {
+                "interface": "eth0",
+                "protocol": "IPV6",
+                "address": "2607:f2c0:f00f:2700:216:3eff:fe1c:9438",
+                "host_veth": ""
+            },
+            {
+                "interface": "lo",
+                "protocol": "IPV4",
+                "address": "127.0.0.1",
+                "host_veth": ""
+            },
+            {
+                "interface": "lo",
+                "protocol": "IPV6",
+                "address": "::1",
+                "host_veth": ""
+            }
+        ]
     }
 
 ### PUT
@@ -511,9 +657,9 @@ HTTP code for this should be 202 (Accepted).
 Input:
 
     {
-        'action': "stop",       # State change action (stop, start, restart, freeze or unfreeze)
-        'timeout': 30,          # A timeout after which the state change is considered as failed
-        'force': true           # Force the state change (currently only valid for stop and restart where it means killing the container)
+        "action": "stop",       # State change action (stop, start, restart, freeze or unfreeze)
+        "timeout": 30,          # A timeout after which the state change is considered as failed
+        "force": true           # Force the state change (currently only valid for stop and restart where it means killing the container)
     }
 
 ## /1.0/containers/\<name\>/files
@@ -529,7 +675,7 @@ The following headers will be set (on top of standard size and mimetype headers)
  * X-LXD-mode: 0700
 
 This is designed to be easily usable from the command line or even a web
-browser. This is only supported for currently running containers.
+browser.
 
 ### POST (?path=/path/inside/the/container)
  * Description: upload a file to the container
@@ -546,7 +692,7 @@ The following headers may be set by the client:
  * X-LXD-mode: 0700
 
 This is designed to be easily usable from the command line or even a web
-browser. This is only supported for currently running containers.
+browser.
 
 ## /1.0/containers/\<name\>/snapshots
 ### GET
@@ -555,6 +701,12 @@ browser. This is only supported for currently running containers.
  * Operation: sync
  * Return: list of URLs for snapshots for this container
 
+Return value:
+
+    [
+        "/1.0/containers/blah/snapshots/snap0"
+    ]
+
 ### POST
  * Description: create a new snapshot
  * Authentication: trusted
@@ -564,8 +716,8 @@ browser. This is only supported for currently running containers.
 Input:
 
     {
-        'name': "my-snapshot",          # Name of the snapshot
-        'stateful': true                # Whether to include state too
+        "name": "my-snapshot",          # Name of the snapshot
+        "stateful": true                # Whether to include state too
     }
 
 ## /1.0/containers/\<name\>/snapshots/\<name\>
@@ -578,9 +730,9 @@ Input:
 Return:
 
     {
-        'creation_date': 1455139453,
-        'name': "my-snapshot",
-        'stateful': true
+        "created_at": "2016-02-16T01:05:05Z",
+        "name": "my-snapshot",
+        "stateful": true
     }
 
 ### POST
@@ -589,18 +741,25 @@ Return:
  * Operation: async
  * Return: background operation or standard error
 
-Input:
+Input (rename the snapshot):
 
     {
-        'name': "new-name"
+        "name": "new-name"
     }
 
-Input (copy snapshot across lxd instances):
+Input (setup the migration source):
 
     {
         "migration": true,
     }
 
+Return (with migration=true):
+
+    {
+        "control": "secret1",       # Migration control socket
+        "fs": "secret3"             # Filesystem transfer socket
+    }
+
 Renaming to an existing name must return the 409 (Conflict) HTTP code.
 
 ### DELETE
@@ -626,23 +785,16 @@ HTTP code for this should be 202 (Accepted).
 Input (run bash):
 
     {
-        'command': ["/bin/bash"],       # Command and arguments
-        'environment': {},              # Optional extra environment variables to set
-        'wait-for-websocket': false,    # Whether to wait for a connection before starting the process
-        'interactive': true             # Whether to allocate a pts device instead of PIPEs
+        "command": ["/bin/bash"],       # Command and arguments
+        "environment": {},              # Optional extra environment variables to set
+        "wait-for-websocket": false,    # Whether to wait for a connection before starting the process
+        "interactive": true             # Whether to allocate a pts device instead of PIPEs
     }
 
 `wait-for-websocket` indicates whether the operation should block and wait for
 a websocket connection to start (so that users can pass stdin and read
 stdout), or simply run to completion with /dev/null as stdin and stdout.
 
-When the exec command finishes, its exit status is available from the
-operation's metadata:
-
-    {
-        'return': 0
-    }
-
 If interactive is set to true, a single websocket is returned and is mapped to a
 pts device for stdin, stdout and stderr of the execed process.
 
@@ -653,20 +805,32 @@ Depending on the state of the interactive flag, one or three different
 websocket/secret pairs will be returned, which are valid for connecting to this
 operations /websocket endpoint.
 
-Response metadata (interactive=true); the output of the pty is mirrored over
-this websocket:
+Return (with wait-for-websocket=true and interactive=false):
 
     {
-        "-1": "secret"
+        "fds": {
+            "0": "f5b6c760c0aa37a6430dd2a00c456430282d89f6e1661a077a926ed1bf3d1c21",
+            "1": "464dcf9f8fdce29d0d6478284523a9f26f4a31ae365d94cd38bac41558b797cf",
+            "2": "25b70415b686360e3b03131e33d6d94ee85a7f19b0f8d141d6dca5a1fc7b00eb",
+            "control": "20c479d9532ab6d6c3060f6cdca07c1f177647c9d96f0c143ab61874160bd8a5"
+        }
     }
 
-Response metadata (interactive=false); each of the process' fds are hooked up
-to these (i.e. you can only write to 0, and read from 1 and 2):
+Return (with wait-for-websocket=true and interactive=true):
 
     {
-        "0": "secret0",
-        "1": "secret1",
-        "2": "secret2",
+        "fds": {
+            "0": "f5b6c760c0aa37a6430dd2a00c456430282d89f6e1661a077a926ed1bf3d1c21",
+            "control": "20c479d9532ab6d6c3060f6cdca07c1f177647c9d96f0c143ab61874160bd8a5"
+        }
+    }
+
+
+When the exec command finishes, its exit status is available from the
+operation's metadata:
+
+    {
+        "return": 0
     }
 
 ## /1.0/containers/\<name\>/logs
@@ -681,8 +845,9 @@ to these (i.e. you can only write to 0, and read from 1 and 2):
 Return:
 
     [
-        'lxc.log',
-        'migration_dump_2015-03-31T14:30:59Z.log'
+        "/1.0/containers/blah/logs/forkstart.log",
+        "/1.0/containers/blah/logs/lxc.conf",
+        "/1.0/containers/blah/logs/lxc.log"
     ]
 
 ## /1.0/containers/\<name\>/logs/\<logfile\>
@@ -713,33 +878,47 @@ Supported arguments are:
  * type: comma separated list of notifications to subscribe to (defaults to all)
 
 The notification types are:
- * operation
- * logging
+ * operation (notification about creation, updates and termination of all background operations)
+ * logging (every log entry from the server)
 
 This never returns. Each notification is sent as a separate JSON dict:
 
     {
-        'timestamp': "2015-06-09T19:07:24.379615253-06:00",                # Current timestamp
-        'type': "operation",                                               # Notification type
-        'metadata': {}                                                     # Extra resource or type specific metadata
+        "timestamp": "2015-06-09T19:07:24.379615253-06:00",                # Current timestamp
+        "type": "operation",                                               # Notification type
+        "metadata": {}                                                     # Extra resource or type specific metadata
     }
 
     {
-        'timestamp': "2015-06-09T19:07:24.379615253-06:00",
-        'type': "logging",
-        'metadata' {'message': "Service started"}
+        "timestamp": "2016-02-17T11:44:28.572721913-05:00",
+        "type": "logging",
+        "metadata": {
+            "context": {
+                "ip": "@",
+                "method": "GET"
+                "url": "/1.0/containers/xen/snapshots",
+            },
+            "level": "info",
+            "message": "handling"
+        }
     }
 
 
 ## /1.0/images
-### GET (?key=value&key1=value1...)
+### GET
  * Description: list of images (public or private)
  * Authentication: guest or trusted
  * Operation: sync
  * Return: list of URLs for images this server publishes
 
-Filtering can be done by specifying a list of key and values in the
-query URL.
+Return:
+
+    [
+        "/1.0/images/54c8caac1f61901ed86c68f24af5f5d3672bdc62c71d04f06df3a59e95684473",
+        "/1.0/images/97d97a3d1d053840ca19c86cdd0596cf1be060c5157d31407f2a4f9f350c78cc",
+        "/1.0/images/a49d26ce5808075f5175bf31f5cb90561f5023dcd408da8ac5e834096d46b2d8",
+        "/1.0/images/c9b6e738fae75286d52f497415463a8ecc61bbcb046536f220d797b0e500a41f"
+    ]
 
 ### POST
  * Description: create and publish a new image
@@ -759,42 +938,50 @@ In the http file upload case, The following headers may be set by the client:
  * X-LXD-public: true/false (defaults to false)
  * X-LXD-properties: URL-encoded key value pairs without duplicate keys (optional properties)
 
-In the source image case, the following dict must be passed:
+In the source image case, the following dict must be used:
 
     {
-        "public": true,                         # true or false
+        "filename": filename,                   # Used for export (optional)
+        "public": true,                         # Whether the image can be downloaded by untrusted users (defaults to false
+        "properties": {                         # Image properties (optional, applied on top of source properties)
+            "os": "Ubuntu"
+        },
         "source": {
             "type": "image",
-            "mode": "pull",                     # One of "pull" or "receive"
+            "mode": "pull",                     # Only pull is supported for now
             "server": "https://10.0.2.3:8443",  # Remote server (pull mode only)
             "secret": "my-secret-string",       # Secret (pull mode only, private images only)
-            'certificate': "PEM certificate",   # Optional PEM certificate. If not mentioned, system CA is used.
+            "certificate": "PEM certificate",   # Optional PEM certificate. If not mentioned, system CA is used.
             "fingerprint": "SHA256",            # Fingerprint of the image (must be set if alias isn't)
             "alias": "ubuntu/devel",            # Name of the alias (must be set if fingerprint isn't)
         }
     }
 
-In the source container case, the following dict must be passed:
+In the source container case, the following dict must be used:
 
     {
-        "public":   true,         # true or false
-        "filename": filename,     # Used for export
+        "filename": filename,     # Used for export (optional)
+        "public":   true,         # Whether the image can be downloaded by untrusted users  (defaults to false)
+        "properties": {           # Image properties (optional)
+            "os": "Ubuntu"
+        },
         "source": {
             "type": "container",  # One of "container" or "snapshot"
             "name": "abc"
-        },
-        "properties": {           # Image properties
-            "os": "Ubuntu",
         }
     }
 
-In the remote image URL case, the following dict must be passed:
+In the remote image URL case, the following dict must be used:
 
     {
-        "public": true,                                    # true or false
+        "filename": filename,                           # Used for export (optional)
+        "public":   true,                               # Whether the image can be downloaded by untrusted users  (defaults to false)
+        "properties": {                                 # Image properties (optional)
+            "os": "Ubuntu"
+        },
         "source": {
             "type": "url",
-            "url": "https://www.some-server.com/image"     # URL for the image
+            "url": "https://www.some-server.com/image"  # URL for the image
         }
     }
 
@@ -804,7 +991,7 @@ which will add the image to the store and possibly do some backend
 filesystem-specific optimizations.
 
 ## /1.0/images/\<fingerprint\>
-### GET (optional secret=SECRET)
+### GET (optional ?secret=SECRET)
  * Description: Image description and metadata
  * Authentication: guest or trusted
  * Operation: sync
@@ -813,18 +1000,26 @@ filesystem-specific optimizations.
 Output:
 
     {
-        'aliases': ['alias1', ...],
-        'architecture': 2,
-        'fingerprint': "a3625aeada09576673620d1d048eed7ac101c4c2409f527a1f77e237fa280e32",
-        'filename': "busybox-amd64.tar.xz",
-        'properties': {
-            'key': 'value'
+        "aliases": [
+            {
+                "name": "trusty",
+                "description": "",
+            }
+        ],
+        "architecture": 2,
+        "fingerprint": "54c8caac1f61901ed86c68f24af5f5d3672bdc62c71d04f06df3a59e95684473",
+        "filename": "ubuntu-trusty-14.04-amd64-server-20160201.tar.xz",
+        "properties": {
+            "architecture": "x86_64",
+            "description": "Ubuntu 14.04 LTS server (20160201)",
+            "os": "ubuntu",
+            "release": "trusty"
         },
-        'public': true,
-        'size': 11031704,
-        'created_at': "2015-06-09T19:07:24.379615253-06:00",
-        'expires_at': "2015-06-19T19:07:24.379615253-06:00",
-        'uploaded_at': "2015-06-09T19:07:24.379615253-06:00"
+        "public": false,
+        "size": 123792592,
+        "created_at": "2016-02-01T21:07:41Z",
+        "expires_at": "1970-01-01T00:00:00Z",
+        "uploaded_at": "2016-02-16T00:44:47Z"
     }
 
 ### DELETE
@@ -849,15 +1044,17 @@ HTTP code for this should be 202 (Accepted).
 Input:
 
     {
-        'properties': {
-            'architecture': "x86_64",
-            'description': "Some description"
+        "properties": {
+            "architecture": "x86_64",
+            "description": "Ubuntu 14.04 LTS server (20160201)",
+            "os": "ubuntu",
+            "release": "trusty"
         },
-        'public': false
+        "public": true,
     }
 
 ## /1.0/images/\<fingerprint\>/export
-### GET (optional secret=SECRET)
+### GET (optional ?secret=SECRET)
  * Description: Download the image tarball
  * Authentication: guest or trusted
  * Operation: sync
@@ -884,12 +1081,18 @@ Input:
     {
     }
 
-Output:
+Return:
+
+    {
+        "secret": "52e9ec5885562aa24d05d7b4846ebb8b5f1f7bf5cd6e285639b569d9eaf54c9b"
+    }
 
 Standard backround operation with "secret" set to the generated secret
 string in metadata.
 
-The operation should be DELETEd once the secret is no longer in use.
+The secret is automatically invalidated 5s after an image URL using it
+has been accessed. This allows to both retried the image information and
+then hit /export with the same secret.
 
 ## /1.0/images/aliases
 ### GET
@@ -898,6 +1101,14 @@ The operation should be DELETEd once the secret is no longer in use.
  * Operation: sync
  * Return: list of URLs for aliases this server knows about
 
+Return:
+
+    [
+        "/1.0/images/aliases/sl6",
+        "/1.0/images/aliases/trusty",
+        "/1.0/images/aliases/xenial"
+    ]
+
 ### POST
  * Description: create a new alias
  * Authentication: trusted
@@ -907,9 +1118,9 @@ The operation should be DELETEd once the secret is no longer in use.
 Input:
 
     {
-        'description': "The alias description",
-        'target': "SHA-256",
-        'name': "alias-name"
+        "description": "The alias description",
+        "target": "SHA-256",
+        "name": "alias-name"
     }
 
 ## /1.0/images/aliases/\<name\>
@@ -921,8 +1132,9 @@ Input:
 
 Output:
     {
-        'description': "The alias description",
-        'target': "SHA-256"
+        "name": "test",
+        "description": "my description",
+        "target": "c9b6e738fae75286d52f497415463a8ecc61bbcb046536f220d797b0e500a41f"
     }
 
 ### PUT
@@ -934,8 +1146,8 @@ Output:
 Input:
 
     {
-        'description': "New description",
-        'target': "SHA-256"
+        "description": "New description",
+        "target": "54c8caac1f61901ed86c68f24af5f5d3672bdc62c71d04f06df3a59e95684473"
     }
 
 ### POST
@@ -947,7 +1159,7 @@ Input:
 Input:
 
     {
-        'name': "new-name"
+        "name": "new-name"
     }
 
 Renaming to an existing name must return the 409 (Conflict) HTTP code.
@@ -971,7 +1183,7 @@ Input (none at present):
  * Return: list of URLs for networks that are current defined on the host
 
     [
-        "/1.0/networks/eth0",
+        "/1.0/networks/eth0",,
         "/1.0/networks/lxcbr0"
     ]
 
@@ -983,9 +1195,11 @@ Input (none at present):
  * Return: dict representing a network
 
     {
-        'name': "lxcbr0",
-        'type': "bridge",
-        'members': ["/1.0/containers/blah"]
+        "name": "lxcbr0",
+        "type": "bridge",
+        "used_by": [
+            "/1.0/containers/blah"
+        ]
     }
 
 ## /1.0/operations
@@ -1010,12 +1224,22 @@ Input (none at present):
 Return:
 
     {
-        'created_at': "2015-06-09T19:07:24.379615253-06:00", # Creation timestamp
-        'updated_at': "2015-06-09T19:07:24.379615253-06:00", # Last update timestamp
-        'status': "Running",
-        'status_code': 103,
-        'metadata': {},                                      # Extra information about the operation (action, target, ...)
-        'may_cancel': true                                   # Whether it's possible to cancel the operation
+        "id": "b8d84888-1dc2-44fd-b386-7f679e171ba5",
+        "class": "token",                                                                       # One of "task" (background task), "websocket" (set of websockets and crendentials) or "token" (temporary credentials)
+        "created_at": "2016-02-17T16:59:27.237628195-05:00",                                    # Creation timestamp
+        "updated_at": "2016-02-17T16:59:27.237628195-05:00",                                    # Last update timestamp
+        "status": "Running",
+        "status_code": 103,
+        "resources": {                                                                          # List of affected resources
+            "images": [
+                "/1.0/images/54c8caac1f61901ed86c68f24af5f5d3672bdc62c71d04f06df3a59e95684473"
+            ]
+        },
+        "metadata": {                                                                           # Extra information about the operation (action, target, ...)
+            "secret": "c9209bee6df99315be1660dd215acde4aec89b8e5336039712fc11008d918b0d"
+        },
+        "may_cancel": true,                                                                     # Whether it's possible to cancel the operation (DELETE)
+        "err": ""
     }
 
 ### DELETE
@@ -1032,18 +1256,18 @@ Input (none at present):
 HTTP code for this should be 202 (Accepted).
 
 ## /1.0/operations/\<uuid\>/wait
-### GET (?status\_code=200&timeout=30)
+### GET (optional ?timeout=30)
  * Description: Wait for an operation to finish
  * Authentication: trusted
  * Operation: sync
- * Return: dict of the operation after its reach its final state
+ * Return: dict of the operation after it's reached its final state
 
 Input (wait indefinitely for a final state): no argument
 
-Input (similar by times out after 30s): ?timeout=30
+Input (similar but times out after 30s): ?timeout=30
 
 ## /1.0/operations/\<uuid\>/websocket
-### GET (?secret=...)
+### GET (?secret=SECRET)
  * Description: This connection is upgraded into a websocket connection
    speaking the protocol defined by the operation type. For example, in the
    case of an exec operation, the websocket is the bidirectional pipe for
@@ -1063,6 +1287,13 @@ Input (similar by times out after 30s): ?timeout=30
  * Operation: sync
  * Return: list of URLs to defined profiles
 
+Return:
+
+    [
+        "/1.0/profiles/default"
+    ]
+
+
 ### POST
  * Description: define a new profile
  * Authentication: trusted
@@ -1072,9 +1303,16 @@ Input (similar by times out after 30s): ?timeout=30
 Input:
 
     {
-        'name': "my-profile'name",
-        'config': {"limits.memory": "2GB"},
-                   "network.0.bridge": "lxcbr0"}
+        "name": "my-profilename",
+        "config": {
+            "limits.memory": "2GB"
+        },
+        "devices": {
+            "kvm": {
+                "type": "unix-char",
+                "path": "/dev/kvm"
+            }
+        }
     }
 
 ## /1.0/profiles/\<name\>
@@ -1087,9 +1325,16 @@ Input:
 Output:
 
     {
-        'name': "my-profile'name",
-        'config': {"limits.memory": "2GB"},
-                   "network.0.bridge": "lxcbr0"}
+        "name": "test",
+        "config": {
+            "limits.memory": "2GB"
+        },
+        "devices": {
+            "kvm": {
+                "path": "/dev/kvm",
+                "type": "unix-char"
+            }
+        }
     }
 
 ### PUT
@@ -1100,6 +1345,18 @@ Output:
 
 Input:
 
+    {
+        "config": {
+            "limits.memory": "4GB"
+        },
+        "devices": {
+            "kvm": {
+                "path": "/dev/kvm",
+                "type": "unix-char"
+            }
+        }
+    }
+
 Same dict as used for initial creation and coming from GET. The name
 property can't be changed (see POST for that).
 
@@ -1113,7 +1370,7 @@ property can't be changed (see POST for that).
 Input (rename a profile):
 
     {
-        'name': "new-name"
+        "name": "new-name"
     }
 
 
@@ -1135,68 +1392,3 @@ Input (none at present):
     }
 
 HTTP code for this should be 202 (Accepted).
-
-## /1.0/certificates
-### GET
- * Description: list of trusted certificates
- * Authentication: trusted
- * Operation: sync
- * Return: list of URLs for trusted certificates
-
-### POST
- * Description: add a new trusted certificate
- * Authentication: trusted or untrusted
- * Operation: sync
- * Return: standard return value or standard error
-
-Input:
-
-    {
-        'type': "client",                       # Certificate type (keyring), currently only client
-        'certificate': "BASE64",                # If provided, a valid x509 certificate. If not, the client certificate of the connection will be used
-        'name': "foo"                           # An optional name for the certificate. If nothing is provided, the host in the TLS header for the request is used.
-        'password': "server-trust-password"     # The trust password for that server (only required if untrusted)
-    }
-
-## /1.0/certificates/\<fingerprint\>
-### GET
- * Description: trusted certificate information
- * Authentication: trusted
- * Operation: sync
- * Return: dict representing a trusted certificate
-
-Output:
-
-    {
-        'type': "client",
-        'certificate': "PEM certificate"
-        'fingerprint': "SHA256 Hash of the raw certificate"
-    }
-
-### DELETE
- * Description: Remove a trusted certificate
- * Authentication: trusted
- * Operation: sync
- * Return: standard return value or standard error
-
-Input (none at present):
-
-    {
-    }
-
-HTTP code for this should be 202 (Accepted).
-
-# Async operations
-Any operation which may take more than a second to be done must be done
-in the background, returning a background operation ID to the client.
-
-The client will then be able to either poll for a status update or wait
-for a notification using the long-poll API.
-
-# Notifications
-A long-poll API is available for notifications, different notification
-types exist to limit the traffic going to the client.
-
-It's recommend 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.


More information about the lxc-devel mailing list