[lxc-devel] [lxd/master] Remaining rc1 features

stgraber on Github lxc-bot at linuxcontainers.org
Wed Mar 2 20:57:00 UTC 2016


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 301 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20160302/12f14e02/attachment.bin>
-------------- next part --------------
From 4bada8452335db79b99a1d914aa439ec97b75424 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 1 Mar 2016 17:28:41 -0500
Subject: [PATCH 1/3] Just use the shared struct whenever possible in the
 client
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 | 57 +++++++++++++++++++++------------------------------------
 1 file changed, 21 insertions(+), 36 deletions(-)

diff --git a/client.go b/client.go
index 29e5d75..ccbddb3 100644
--- a/client.go
+++ b/client.go
@@ -370,7 +370,7 @@ func (c *Client) baseGet(getUrl string) (*Response, error) {
 	return HoistResponse(resp, Sync)
 }
 
-func (c *Client) put(base string, args shared.Jmap, rtype ResponseType) (*Response, error) {
+func (c *Client) put(base string, args interface{}, rtype ResponseType) (*Response, error) {
 	uri := c.url(shared.APIVersion, base)
 
 	buf := bytes.Buffer{}
@@ -396,7 +396,7 @@ func (c *Client) put(base string, args shared.Jmap, rtype ResponseType) (*Respon
 	return HoistResponse(resp, rtype)
 }
 
-func (c *Client) post(base string, args shared.Jmap, rtype ResponseType) (*Response, error) {
+func (c *Client) post(base string, args interface{}, rtype ResponseType) (*Response, error) {
 	uri := c.url(shared.APIVersion, base)
 
 	buf := bytes.Buffer{}
@@ -446,7 +446,7 @@ func (c *Client) getRaw(uri string) (*http.Response, error) {
 	return raw, nil
 }
 
-func (c *Client) delete(base string, args shared.Jmap, rtype ResponseType) (*Response, error) {
+func (c *Client) delete(base string, args interface{}, rtype ResponseType) (*Response, error) {
 	uri := c.url(shared.APIVersion, base)
 
 	buf := bytes.Buffer{}
@@ -1031,11 +1031,7 @@ func (c *Client) GetImageInfo(image string) (*shared.ImageInfo, error) {
 }
 
 func (c *Client) PutImageInfo(name string, p shared.BriefImageInfo) error {
-	body := shared.Jmap{}
-	body["public"] = p.Public
-	body["properties"] = p.Properties
-
-	_, err := c.put(fmt.Sprintf("images/%s", name), body, Sync)
+	_, err := c.put(fmt.Sprintf("images/%s", name), p, Sync)
 	return err
 }
 
@@ -1730,15 +1726,12 @@ func (c *Client) SetServerConfig(key string, value string) (*Response, error) {
 	}
 
 	ss.Config[key] = value
-	body := shared.Jmap{"config": ss.Config}
 
-	return c.put("", body, Sync)
+	return c.put("", ss, Sync)
 }
 
 func (c *Client) UpdateServerConfig(ss shared.BriefServerState) (*Response, error) {
-	body := shared.Jmap{"config": ss.Config}
-
-	return c.put("", body, Sync)
+	return c.put("", ss, Sync)
 }
 
 /*
@@ -1776,13 +1769,12 @@ func (c *Client) SetContainerConfig(container, key, value string) error {
 		st.Config[key] = value
 	}
 
-	body := shared.Jmap{"config": st.Config, "profiles": st.Profiles, "name": container, "devices": st.Devices}
 	/*
 	 * Although container config is an async operation (we PUT to restore a
 	 * snapshot), we expect config to be a sync operation, so let's just
 	 * handle it here.
 	 */
-	resp, err := c.put(fmt.Sprintf("containers/%s", container), body, Async)
+	resp, err := c.put(fmt.Sprintf("containers/%s", container), st, Async)
 	if err != nil {
 		return err
 	}
@@ -1791,12 +1783,7 @@ func (c *Client) SetContainerConfig(container, key, value string) error {
 }
 
 func (c *Client) UpdateContainerConfig(container string, st shared.BriefContainerInfo) error {
-	body := shared.Jmap{"name": container,
-		"profiles":  st.Profiles,
-		"config":    st.Config,
-		"devices":   st.Devices,
-		"ephemeral": st.Ephemeral}
-	resp, err := c.put(fmt.Sprintf("containers/%s", container), body, Async)
+	resp, err := c.put(fmt.Sprintf("containers/%s", container), st, Async)
 	if err != nil {
 		return err
 	}
@@ -1838,8 +1825,7 @@ func (c *Client) SetProfileConfigItem(profile, key, value string) error {
 		st.Config[key] = value
 	}
 
-	body := shared.Jmap{"name": profile, "config": st.Config, "devices": st.Devices}
-	_, err = c.put(fmt.Sprintf("profiles/%s", profile), body, Sync)
+	_, err = c.put(fmt.Sprintf("profiles/%s", profile), st, Sync)
 	return err
 }
 
@@ -1847,8 +1833,8 @@ func (c *Client) PutProfile(name string, profile shared.ProfileConfig) error {
 	if profile.Name != name {
 		return fmt.Errorf("Cannot change profile name")
 	}
-	body := shared.Jmap{"name": name, "description": profile.Description, "config": profile.Config, "devices": profile.Devices}
-	_, err := c.put(fmt.Sprintf("profiles/%s", name), body, Sync)
+
+	_, err := c.put(fmt.Sprintf("profiles/%s", name), profile, Sync)
 	return err
 }
 
@@ -1894,10 +1880,10 @@ func (c *Client) ApplyProfile(container, profile string) (*Response, error) {
 	if err != nil {
 		return nil, err
 	}
-	profiles := strings.Split(profile, ",")
-	body := shared.Jmap{"config": st.Config, "profiles": profiles, "name": st.Name, "devices": st.Devices}
 
-	return c.put(fmt.Sprintf("containers/%s", container), body, Async)
+	st.Profiles = strings.Split(profile, ",")
+
+	return c.put(fmt.Sprintf("containers/%s", container), st, Async)
 }
 
 func (c *Client) ContainerDeviceDelete(container, devname string) (*Response, error) {
@@ -1908,8 +1894,7 @@ func (c *Client) ContainerDeviceDelete(container, devname string) (*Response, er
 
 	delete(st.Devices, devname)
 
-	body := shared.Jmap{"config": st.Config, "profiles": st.Profiles, "name": st.Name, "devices": st.Devices}
-	return c.put(fmt.Sprintf("containers/%s", container), body, Async)
+	return c.put(fmt.Sprintf("containers/%s", container), st, Async)
 }
 
 func (c *Client) ContainerDeviceAdd(container, devname, devtype string, props []string) (*Response, error) {
@@ -1928,17 +1913,19 @@ func (c *Client) ContainerDeviceAdd(container, devname, devtype string, props []
 		v := results[1]
 		newdev[k] = v
 	}
+
 	if st.Devices != nil && st.Devices.ContainsName(devname) {
 		return nil, fmt.Errorf("device already exists")
 	}
+
 	newdev["type"] = devtype
 	if st.Devices == nil {
 		st.Devices = shared.Devices{}
 	}
+
 	st.Devices[devname] = newdev
 
-	body := shared.Jmap{"config": st.Config, "profiles": st.Profiles, "name": st.Name, "devices": st.Devices}
-	return c.put(fmt.Sprintf("containers/%s", container), body, Async)
+	return c.put(fmt.Sprintf("containers/%s", container), st, Async)
 }
 
 func (c *Client) ContainerListDevices(container string) ([]string, error) {
@@ -1965,8 +1952,7 @@ func (c *Client) ProfileDeviceDelete(profile, devname string) (*Response, error)
 		}
 	}
 
-	body := shared.Jmap{"config": st.Config, "name": st.Name, "devices": st.Devices}
-	return c.put(fmt.Sprintf("profiles/%s", profile), body, Sync)
+	return c.put(fmt.Sprintf("profiles/%s", profile), st, Sync)
 }
 
 func (c *Client) ProfileDeviceAdd(profile, devname, devtype string, props []string) (*Response, error) {
@@ -1994,8 +1980,7 @@ func (c *Client) ProfileDeviceAdd(profile, devname, devtype string, props []stri
 	}
 	st.Devices[devname] = newdev
 
-	body := shared.Jmap{"config": st.Config, "name": st.Name, "devices": st.Devices}
-	return c.put(fmt.Sprintf("profiles/%s", profile), body, Sync)
+	return c.put(fmt.Sprintf("profiles/%s", profile), st, Sync)
 }
 
 func (c *Client) ProfileListDevices(profile string) ([]string, error) {

From b5c6afd6ad53645a79d6ea9e0320f90b7cfac638 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 2 Mar 2016 00:20:00 -0500
Subject: [PATCH 2/3] Fix download progress on launch
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/init.go | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/lxc/init.go b/lxc/init.go
index 96830cd..27446f5 100644
--- a/lxc/init.go
+++ b/lxc/init.go
@@ -237,7 +237,6 @@ func (c *initCmd) initProgressTracker(d *lxd.Client, operation string) {
 		}
 
 		if shared.StatusCode(md["status_code"].(float64)).IsFinal() {
-			fmt.Printf("\n")
 			return
 		}
 
@@ -246,6 +245,10 @@ func (c *initCmd) initProgressTracker(d *lxd.Client, operation string) {
 		if ok {
 			fmt.Printf(i18n.G("Retrieving image: %s")+"\r", opMd["download_progress"].(string))
 		}
+
+		if opMd["download_progress"].(string) == "100%" {
+			fmt.Printf("\n")
+		}
 	}
 	go d.Monitor([]string{"operation"}, handler)
 }

From 28e7d344175097877af7747fd5f2926e9965b17f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 1 Mar 2016 01:07:52 -0500
Subject: [PATCH 3/3] Implement image auto-update
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This introduces everything that's needed to keep images up to date within LXD.

Adds source tracking, a flag on image to indicate whether they should be
auto-synced and configuration to set the interval between updates and
the behavior for automatically cached images.

Closes #1679

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 client.go                      |   4 +-
 lxc/image.go                   |  25 ++++++--
 lxc/publish.go                 |   2 +-
 lxd/container.go               |   2 +-
 lxd/containers_post.go         |  15 ++---
 lxd/daemon.go                  |  99 +++++++++++++++-------------
 lxd/daemon_images.go           |  50 +++++++++++++--
 lxd/db.go                      |  12 +++-
 lxd/db_images.go               | 142 +++++++++++++++++++++++++++++++----------
 lxd/db_update.go               |  23 +++++++
 lxd/images.go                  | 125 +++++++++++++++++++++++++++++-------
 po/lxd.pot                     |  96 +++++++++++++++++-----------
 shared/image.go                |  23 +++++--
 specs/configuration.md         |   2 +
 specs/database.md              |  16 +++++
 specs/rest-api.md              |  10 ++-
 test/suites/database_update.sh |   4 +-
 17 files changed, 487 insertions(+), 163 deletions(-)

diff --git a/client.go b/client.go
index ccbddb3..a6bd895 100644
--- a/client.go
+++ b/client.go
@@ -565,7 +565,7 @@ func (c *Client) ListContainers() ([]shared.ContainerInfo, error) {
 	return result, nil
 }
 
-func (c *Client) CopyImage(image string, dest *Client, copy_aliases bool, aliases []string, public bool, progressHandler func(progress string)) error {
+func (c *Client) CopyImage(image string, dest *Client, copy_aliases bool, aliases []string, public bool, autoUpdate bool, progressHandler func(progress string)) error {
 	source := shared.Jmap{
 		"type":        "image",
 		"mode":        "pull",
@@ -652,7 +652,7 @@ func (c *Client) CopyImage(image string, dest *Client, copy_aliases bool, aliase
 		sourceUrl := "https://" + addr
 
 		source["server"] = sourceUrl
-		body := shared.Jmap{"public": public, "source": source}
+		body := shared.Jmap{"public": public, "auto_update": autoUpdate, "source": source}
 
 		resp, err := dest.post("images", body, Async)
 		if err != nil {
diff --git a/lxc/image.go b/lxc/image.go
index a4e3745..409fa8e 100644
--- a/lxc/image.go
+++ b/lxc/image.go
@@ -72,6 +72,7 @@ type imageCmd struct {
 	addAliases  aliasList
 	publicImage bool
 	copyAliases bool
+	autoUpdate  bool
 }
 
 func (c *imageCmd) showByDefault() bool {
@@ -110,9 +111,12 @@ hash or alias name (if one is set).
 lxc image import <tarball> [rootfs tarball|URL] [remote:] [--public] [--created-at=ISO-8601] [--expires-at=ISO-8601] [--fingerprint=FINGERPRINT] [prop=value]
     Import an image tarball (or tarballs) into the LXD image store.
 
-lxc image copy [remote:]<image> <remote>: [--alias=ALIAS].. [--copy-aliases] [--public]
+lxc image copy [remote:]<image> <remote>: [--alias=ALIAS].. [--copy-aliases] [--public] [--auto-update]
     Copy an image from one LXD daemon to another over the network.
 
+    The auto-update flag instructs the server to keep this image up to
+    date. It requires the source to be an alias and for it to be public.
+
 lxc image delete [remote:]<image>
     Delete an image from the LXD image store.
 
@@ -149,6 +153,7 @@ lxc image alias list [remote:]
 func (c *imageCmd) flags() {
 	gnuflag.BoolVar(&c.publicImage, "public", false, i18n.G("Make image public"))
 	gnuflag.BoolVar(&c.copyAliases, "copy-aliases", false, i18n.G("Copy aliases from source"))
+	gnuflag.BoolVar(&c.autoUpdate, "auto-update", false, i18n.G("Keep the image up to date after initial copy"))
 	gnuflag.Var(&c.addAliases, "alias", i18n.G("New alias to define at target"))
 }
 
@@ -245,7 +250,7 @@ func (c *imageCmd) run(config *lxd.Config, args []string) error {
 			fmt.Printf(i18n.G("Copying the image: %s")+"\r", progress)
 		}
 
-		err = d.CopyImage(inName, dest, c.copyAliases, c.addAliases, c.publicImage, progressHandler)
+		err = d.CopyImage(inName, dest, c.copyAliases, c.addAliases, c.publicImage, c.autoUpdate, progressHandler)
 		if err == nil {
 			fmt.Println(i18n.G("Image copied successfully!"))
 		}
@@ -286,13 +291,18 @@ func (c *imageCmd) run(config *lxd.Config, args []string) error {
 		if err != nil {
 			return err
 		}
-		fmt.Printf(i18n.G("Fingerprint: %s")+"\n", info.Fingerprint)
-		public := i18n.G("no")
 
+		public := i18n.G("no")
 		if info.Public {
 			public = i18n.G("yes")
 		}
 
+		autoUpdate := i18n.G("disabled")
+		if info.AutoUpdate {
+			autoUpdate = i18n.G("enabled")
+		}
+
+		fmt.Printf(i18n.G("Fingerprint: %s")+"\n", info.Fingerprint)
 		fmt.Printf(i18n.G("Size: %.2fMB")+"\n", float64(info.Size)/1024.0/1024.0)
 		fmt.Printf(i18n.G("Architecture: %s")+"\n", info.Architecture)
 		fmt.Printf(i18n.G("Public: %s")+"\n", public)
@@ -315,6 +325,13 @@ func (c *imageCmd) run(config *lxd.Config, args []string) error {
 		for _, alias := range info.Aliases {
 			fmt.Printf("    - %s\n", alias.Name)
 		}
+		fmt.Printf(i18n.G("Auto update: %s")+"\n", autoUpdate)
+		if info.Source != nil {
+			fmt.Println(i18n.G("Source:"))
+			fmt.Printf("    Server: %s\n", info.Source.Server)
+			fmt.Printf("    Protocol: %s\n", info.Source.Protocol)
+			fmt.Printf("    Alias: %s\n", info.Source.Alias)
+		}
 		return nil
 
 	case "import":
diff --git a/lxc/publish.go b/lxc/publish.go
index 690dfdf..b09675c 100644
--- a/lxc/publish.go
+++ b/lxc/publish.go
@@ -148,7 +148,7 @@ func (c *publishCmd) run(config *lxd.Config, args []string) error {
 	}
 	defer s.DeleteImage(fp)
 
-	err = s.CopyImage(fp, d, false, c.pAliases, c.makePublic, nil)
+	err = s.CopyImage(fp, d, false, c.pAliases, c.makePublic, false, nil)
 	if err != nil {
 		return err
 	}
diff --git a/lxd/container.go b/lxd/container.go
index 99f6723..4d7638e 100644
--- a/lxd/container.go
+++ b/lxd/container.go
@@ -432,7 +432,7 @@ func containerCreateFromImage(d *Daemon, args containerArgs, hash string) (conta
 		return nil, err
 	}
 
-	if err := dbImageLastAccessUpdate(d.db, hash); err != nil {
+	if err := dbImageLastAccessUpdate(d.db, hash, time.Now().UTC()); err != nil {
 		return nil, fmt.Errorf("Error updating image last use date: %s", err)
 	}
 
diff --git a/lxd/containers_post.go b/lxd/containers_post.go
index b0f12d1..11a01d5 100644
--- a/lxd/containers_post.go
+++ b/lxd/containers_post.go
@@ -58,14 +58,12 @@ func createFromImage(d *Daemon, req *containerPostReq) Response {
 	var hash string
 	var err error
 
-	if req.Source.Alias != "" {
-		if req.Source.Mode == "pull" && req.Source.Server != "" {
-			hash, err = remoteGetImageFingerprint(d, req.Source.Server, req.Source.Certificate, req.Source.Alias)
-			if err != nil {
-				return InternalError(err)
-			}
+	if req.Source.Fingerprint != "" {
+		hash = req.Source.Fingerprint
+	} else if req.Source.Alias != "" {
+		if req.Source.Server != "" {
+			hash = req.Source.Alias
 		} else {
-
 			_, alias, err := dbImageAliasGet(d.db, req.Source.Alias, true)
 			if err != nil {
 				return InternalError(err)
@@ -81,7 +79,8 @@ func createFromImage(d *Daemon, req *containerPostReq) Response {
 
 	run := func(op *operation) error {
 		if req.Source.Server != "" {
-			hash, err = d.ImageDownload(op, req.Source.Server, req.Source.Protocol, req.Source.Certificate, req.Source.Secret, hash, true)
+			updateCached, _ := d.ConfigValueGet("images.auto_update_cached")
+			hash, err = d.ImageDownload(op, req.Source.Server, req.Source.Protocol, req.Source.Certificate, req.Source.Secret, hash, true, updateCached != "false")
 			if err != nil {
 				return err
 			}
diff --git a/lxd/daemon.go b/lxd/daemon.go
index 49a911b..83a3686 100644
--- a/lxd/daemon.go
+++ b/lxd/daemon.go
@@ -66,18 +66,19 @@ type Socket struct {
 
 // A Daemon can respond to requests from a shared client.
 type Daemon struct {
-	architectures []int
-	BackingFs     string
-	clientCerts   []x509.Certificate
-	db            *sql.DB
-	group         string
-	IdmapSet      *shared.IdmapSet
-	lxcpath       string
-	mux           *mux.Router
-	tomb          tomb.Tomb
-	pruneChan     chan bool
-	shutdownChan  chan bool
-	execPath      string
+	architectures       []int
+	BackingFs           string
+	clientCerts         []x509.Certificate
+	db                  *sql.DB
+	group               string
+	IdmapSet            *shared.IdmapSet
+	lxcpath             string
+	mux                 *mux.Router
+	tomb                tomb.Tomb
+	pruneChan           chan bool
+	shutdownChan        chan bool
+	resetAutoUpdateChan chan bool
+	execPath            string
 
 	Storage storage
 
@@ -583,35 +584,6 @@ func (d *Daemon) UpdateHTTPsPort(oldAddress string, newAddress string) error {
 	return nil
 }
 
-func (d *Daemon) pruneExpiredImages() {
-	shared.Debugf("Pruning expired images")
-	expiry, err := dbImageExpiryGet(d.db)
-	if err != nil { // no expiry
-		shared.Debugf("Failed getting the cached image expiry timeout")
-		return
-	}
-
-	q := `
-SELECT fingerprint FROM images WHERE cached=1 AND creation_date<=strftime('%s', date('now', '-` + expiry + ` day'))`
-	inargs := []interface{}{}
-	var fingerprint string
-	outfmt := []interface{}{fingerprint}
-
-	result, err := dbQueryScan(d.db, q, inargs, outfmt)
-	if err != nil {
-		shared.Debugf("Error making cache expiry query: %s", err)
-		return
-	}
-	shared.Debugf("Found %d expired images", len(result))
-
-	for _, r := range result {
-		if err := doDeleteImage(d, r[0].(string)); err != nil {
-			shared.Debugf("Error deleting image: %s", err)
-		}
-	}
-	shared.Debugf("Done pruning expired images")
-}
-
 // StartDaemon starts the shared daemon with the provided configuration.
 func startDaemon(group string) (*Daemon, error) {
 	d := &Daemon{
@@ -834,22 +806,57 @@ func (d *Daemon) Init() error {
 	/* Prune images */
 	d.pruneChan = make(chan bool)
 	go func() {
-		d.pruneExpiredImages()
+		pruneExpiredImages(d)
 		for {
 			timer := time.NewTimer(24 * time.Hour)
 			timeChan := timer.C
 			select {
 			case <-timeChan:
 				/* run once per day */
-				d.pruneExpiredImages()
+				pruneExpiredImages(d)
 			case <-d.pruneChan:
 				/* run when image.remote_cache_expiry is changed */
-				d.pruneExpiredImages()
+				pruneExpiredImages(d)
 				timer.Stop()
 			}
 		}
 	}()
 
+	/* Auto-update images */
+	d.resetAutoUpdateChan = make(chan bool)
+	go func() {
+		autoUpdateImages(d)
+
+		for {
+			interval, _ := d.ConfigValueGet("images.auto_update_interval")
+			if interval == "" {
+				interval = "6"
+			}
+
+			intervalInt, err := strconv.Atoi(interval)
+			if err != nil {
+				intervalInt = 0
+			}
+
+			if intervalInt > 0 {
+				timer := time.NewTimer(time.Duration(intervalInt) * time.Hour)
+				timeChan := timer.C
+
+				select {
+				case <-timeChan:
+					autoUpdateImages(d)
+				case <-d.resetAutoUpdateChan:
+					timer.Stop()
+				}
+			} else {
+				select {
+				case <-d.resetAutoUpdateChan:
+					continue
+				}
+			}
+		}
+	}()
+
 	/* Setup /dev/lxd */
 	d.devlxd, err = createAndBindDevLxd()
 	if err != nil {
@@ -1141,6 +1148,10 @@ func (d *Daemon) ConfigKeyIsValid(key string) bool {
 		return true
 	case "images.compression_algorithm":
 		return true
+	case "images.auto_update_interval":
+		return true
+	case "images.auto_update_cached":
+		return true
 	}
 
 	return false
diff --git a/lxd/daemon_images.go b/lxd/daemon_images.go
index 9773143..43efa77 100644
--- a/lxd/daemon_images.go
+++ b/lxd/daemon_images.go
@@ -17,19 +17,36 @@ import (
 
 // ImageDownload checks if we have that Image Fingerprint else
 // downloads the image from a remote server.
-func (d *Daemon) ImageDownload(op *operation, server string, protocol string, certificate string, secret string, fp string, forContainer bool) (string, error) {
+func (d *Daemon) ImageDownload(op *operation, server string, protocol string, certificate string, secret string, alias string, forContainer bool, autoUpdate bool) (string, error) {
 	var err error
 	var ss *shared.SimpleStreams
+	fp := alias
 
+	// Expand aliases
 	if protocol == "simplestreams" {
 		ss, err = shared.SimpleStreamsClient(server)
 		if err != nil {
 			return "", err
 		}
 
-		fp = ss.GetAlias(fp)
-		if fp == "" {
-			return "", fmt.Errorf("The requested image couldn't be found.")
+		target := ss.GetAlias(fp)
+		if target != "" {
+			fp = target
+		}
+
+		image, err := ss.GetImageInfo(fp)
+		if err != nil {
+			return "", err
+		}
+
+		if fp == alias {
+			alias = image.Fingerprint
+		}
+		fp = image.Fingerprint
+	} else if protocol == "lxd" {
+		target, err := remoteGetImageFingerprint(d, server, certificate, fp)
+		if err == nil && target != "" {
+			fp = target
 		}
 	}
 
@@ -175,12 +192,25 @@ func (d *Daemon) ImageDownload(op *operation, server string, protocol string, ce
 		}
 
 		info.Public = false
+		info.AutoUpdate = autoUpdate
 
 		_, err = imageBuildFromInfo(d, *info)
 		if err != nil {
 			return "", err
 		}
 
+		if alias != fp {
+			id, _, err := dbImageGet(d.db, fp, false, true)
+			if err != nil {
+				return "", err
+			}
+
+			err = dbImageSourceInsert(d.db, id, server, protocol, "", alias)
+			if err != nil {
+				return "", err
+			}
+		}
+
 		if forContainer {
 			return fp, dbImageLastAccessInit(d.db, fp)
 		}
@@ -320,6 +350,18 @@ func (d *Daemon) ImageDownload(op *operation, server string, protocol string, ce
 	// By default, make all downloaded images private
 	info.Public = false
 
+	if alias != fp {
+		id, _, err := dbImageGet(d.db, fp, false, true)
+		if err != nil {
+			return "", err
+		}
+
+		err = dbImageSourceInsert(d.db, id, server, protocol, "", alias)
+		if err != nil {
+			return "", err
+		}
+	}
+
 	_, err = imageBuildFromInfo(d, info)
 	if err != nil {
 		shared.Log.Error(
diff --git a/lxd/db.go b/lxd/db.go
index 7bed7f4..75dfae5 100644
--- a/lxd/db.go
+++ b/lxd/db.go
@@ -34,7 +34,7 @@ type Profile struct {
 // Profiles will contain a list of all Profiles.
 type Profiles []Profile
 
-const DB_CURRENT_VERSION int = 26
+const DB_CURRENT_VERSION int = 27
 
 // CURRENT_SCHEMA contains the current SQLite SQL Schema.
 const CURRENT_SCHEMA string = `
@@ -102,6 +102,7 @@ CREATE TABLE IF NOT EXISTS images (
     filename VARCHAR(255) NOT NULL,
     size INTEGER NOT NULL,
     public INTEGER NOT NULL DEFAULT 0,
+    auto_update INTEGER NOT NULL DEFAULT 0,
     architecture INTEGER NOT NULL,
     creation_date DATETIME,
     expiry_date DATETIME,
@@ -125,6 +126,15 @@ CREATE TABLE IF NOT EXISTS images_properties (
     value TEXT,
     FOREIGN KEY (image_id) REFERENCES images (id) ON DELETE CASCADE
 );
+CREATE TABLE IF NOT EXISTS images_source (
+    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+    image_id INTEGER NOT NULL,
+    server TEXT NOT NULL,
+    protocol INTEGER NOT NULL,
+    certificate TEXT NOT NULL,
+    alias VARCHAR(255) NOT NULL,
+    FOREIGN KEY (image_id) REFERENCES images (id) ON DELETE CASCADE
+);
 CREATE TABLE IF NOT EXISTS profiles (
     id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
     name VARCHAR(255) NOT NULL,
diff --git a/lxd/db_images.go b/lxd/db_images.go
index c845b46..efaefd4 100644
--- a/lxd/db_images.go
+++ b/lxd/db_images.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"database/sql"
+	"fmt"
 	"time"
 
 	_ "github.com/mattn/go-sqlite3"
@@ -9,6 +10,12 @@ import (
 	"github.com/lxc/lxd/shared"
 )
 
+var dbImageSourceProtocol = map[int]string{
+	0: "lxd",
+	1: "direct",
+	2: "simplestreams",
+}
+
 func dbImagesGet(db *sql.DB, public bool) ([]string, error) {
 	q := "SELECT fingerprint FROM images"
 	if public == true {
@@ -31,6 +38,72 @@ func dbImagesGet(db *sql.DB, public bool) ([]string, error) {
 	return results, nil
 }
 
+func dbImagesGetExpired(db *sql.DB, expiry int) ([]string, error) {
+	q := `SELECT fingerprint FROM images WHERE cached=1 AND creation_date<=strftime('%s', date('now', '-` + fmt.Sprintf("%d", expiry) + ` day'))`
+
+	var fp string
+	inargs := []interface{}{}
+	outfmt := []interface{}{fp}
+	dbResults, err := dbQueryScan(db, q, inargs, outfmt)
+	if err != nil {
+		return []string{}, err
+	}
+
+	results := []string{}
+	for _, r := range dbResults {
+		results = append(results, r[0].(string))
+	}
+
+	return results, nil
+}
+
+func dbImageSourceInsert(db *sql.DB, imageId int, server string, protocol string, certificate string, alias string) error {
+	stmt := `INSERT INTO images_source (image_id, server, protocol, certificate, alias) values (?, ?, ?, ?, ?)`
+
+	protocolInt := -1
+	for protoInt, protoString := range dbImageSourceProtocol {
+		if protoString == protocol {
+			protocolInt = protoInt
+		}
+	}
+
+	if protocolInt == -1 {
+		return fmt.Errorf("Invalid protocol: %s", protocol)
+	}
+
+	_, err := dbExec(db, stmt, imageId, server, protocolInt, certificate, alias)
+	return err
+}
+
+func dbImageSourceGet(db *sql.DB, imageId int) (int, shared.ImageSource, error) {
+	q := `SELECT id, server, protocol, certificate, alias FROM images_source WHERE image_id=?`
+
+	id := 0
+	protocolInt := -1
+	result := shared.ImageSource{}
+
+	arg1 := []interface{}{imageId}
+	arg2 := []interface{}{&id, &result.Server, &protocolInt, &result.Certificate, &result.Alias}
+	err := dbQueryRowScan(db, q, arg1, arg2)
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return -1, shared.ImageSource{}, NoSuchObjectError
+		}
+
+		return -1, shared.ImageSource{}, err
+	}
+
+	protocol, found := dbImageSourceProtocol[protocolInt]
+	if !found {
+		return -1, shared.ImageSource{}, fmt.Errorf("Invalid protocol: %d", protocolInt)
+	}
+
+	result.Protocol = protocol
+
+	return id, result, nil
+
+}
+
 // dbImageGet gets an ImageBaseInfo object from the database.
 // The argument fingerprint will be queried with a LIKE query, means you can
 // pass a shortform and will get the full fingerprint.
@@ -47,7 +120,7 @@ func dbImageGet(db *sql.DB, fingerprint string, public bool, strictMatching bool
 
 	// These two humongous things will be filled by the call to DbQueryRowScan
 	outfmt := []interface{}{&id, &image.Fingerprint, &image.Filename,
-		&image.Size, &image.Cached, &image.Public, &arch,
+		&image.Size, &image.Cached, &image.Public, &image.AutoUpdate, &arch,
 		&create, &expire, &used, &upload}
 
 	var query string
@@ -57,7 +130,7 @@ func dbImageGet(db *sql.DB, fingerprint string, public bool, strictMatching bool
 		inargs = []interface{}{fingerprint}
 		query = `
         SELECT
-            id, fingerprint, filename, size, cached, public, architecture,
+            id, fingerprint, filename, size, cached, public, auto_update, architecture,
             creation_date, expiry_date, last_use_date, upload_date
         FROM
             images
@@ -66,7 +139,7 @@ func dbImageGet(db *sql.DB, fingerprint string, public bool, strictMatching bool
 		inargs = []interface{}{fingerprint + "%"}
 		query = `
         SELECT
-            id, fingerprint, filename, size, cached, public, architecture,
+            id, fingerprint, filename, size, cached, public, auto_update, architecture,
             creation_date, expiry_date, last_use_date, upload_date
         FROM
             images
@@ -145,6 +218,11 @@ func dbImageGet(db *sql.DB, fingerprint string, public bool, strictMatching bool
 
 	image.Aliases = aliases
 
+	_, source, err := dbImageSourceGet(db, id)
+	if err == nil {
+		image.Source = &source
+	}
+
 	return id, &image, nil
 }
 
@@ -156,6 +234,7 @@ func dbImageDelete(db *sql.DB, id int) error {
 
 	_, _ = tx.Exec("DELETE FROM images_aliases WHERE image_id=?", id)
 	_, _ = tx.Exec("DELETE FROM images_properties WHERE image_id=?", id)
+	_, _ = tx.Exec("DELETE FROM images_source WHERE image_id=?", id)
 	_, _ = tx.Exec("DELETE FROM images WHERE id=?", id)
 
 	if err := txCommit(tx); err != nil {
@@ -202,6 +281,11 @@ func dbImageAliasDelete(db *sql.DB, name string) error {
 	return err
 }
 
+func dbImageAliasesMove(db *sql.DB, source int, destination int) error {
+	_, err := dbExec(db, "UPDATE images_aliases SET image_id=? WHERE image_id=?", destination, source)
+	return err
+}
+
 // Insert an alias ento the database.
 func dbImageAliasAdd(db *sql.DB, name string, imageID int, desc string) error {
 	stmt := `INSERT INTO images_aliases (name, image_id, description) values (?, ?, ?)`
@@ -215,9 +299,9 @@ func dbImageAliasUpdate(db *sql.DB, id int, imageID int, desc string) error {
 	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)
+func dbImageLastAccessUpdate(db *sql.DB, fingerprint string, date time.Time) error {
+	stmt := `UPDATE images SET last_use_date=? WHERE fingerprint=?`
+	_, err := dbExec(db, stmt, date, fingerprint)
 	return err
 }
 
@@ -227,23 +311,7 @@ func dbImageLastAccessInit(db *sql.DB, fingerprint string) error {
 	return err
 }
 
-func dbImageExpiryGet(db *sql.DB) (string, error) {
-	q := `SELECT value FROM config WHERE key='images.remote_cache_expiry'`
-	arg1 := []interface{}{}
-	var expiry string
-	arg2 := []interface{}{&expiry}
-	err := dbQueryRowScan(db, q, arg1, arg2)
-	switch err {
-	case sql.ErrNoRows:
-		return "10", nil
-	case nil:
-		return expiry, nil
-	default:
-		return "", err
-	}
-}
-
-func dbImageUpdate(db *sql.DB, id int, fname string, sz int64, public bool, architecture string, creationDate time.Time, expiryDate time.Time, properties map[string]string) error {
+func dbImageUpdate(db *sql.DB, id int, fname string, sz int64, public bool, autoUpdate bool, architecture string, creationDate time.Time, expiryDate time.Time, properties map[string]string) error {
 	arch, err := shared.ArchitectureId(architecture)
 	if err != nil {
 		arch = 0
@@ -254,19 +322,24 @@ func dbImageUpdate(db *sql.DB, id int, fname string, sz int64, public bool, arch
 		return err
 	}
 
-	sqlPublic := 0
+	publicInt := 0
 	if public {
-		sqlPublic = 1
+		publicInt = 1
 	}
 
-	stmt, err := tx.Prepare(`UPDATE images SET filename=?, size=?, public=?, architecture=?, creation_date=?, expiry_date=? WHERE id=?`)
+	autoUpdateInt := 0
+	if autoUpdate {
+		autoUpdateInt = 1
+	}
+
+	stmt, err := tx.Prepare(`UPDATE images SET filename=?, size=?, public=?, auto_update=?, 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)
+	_, err = stmt.Exec(fname, sz, publicInt, autoUpdateInt, arch, creationDate, expiryDate, id)
 	if err != nil {
 		tx.Rollback()
 		return err
@@ -295,7 +368,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, architecture string, creationDate time.Time, expiryDate time.Time, properties map[string]string) error {
+func dbImageInsert(db *sql.DB, fp string, fname string, sz int64, public bool, autoUpdate bool, architecture string, creationDate time.Time, expiryDate time.Time, properties map[string]string) error {
 	arch, err := shared.ArchitectureId(architecture)
 	if err != nil {
 		arch = 0
@@ -306,19 +379,24 @@ func dbImageInsert(db *sql.DB, fp string, fname string, sz int64, public bool, a
 		return err
 	}
 
-	sqlPublic := 0
+	publicInt := 0
 	if public {
-		sqlPublic = 1
+		publicInt = 1
+	}
+
+	autoUpdateInt := 0
+	if autoUpdate {
+		autoUpdateInt = 1
 	}
 
-	stmt, err := tx.Prepare(`INSERT INTO images (fingerprint, filename, size, public, architecture, creation_date, expiry_date, upload_date) VALUES (?, ?, ?, ?, ?, ?, ?, strftime("%s"))`)
+	stmt, err := tx.Prepare(`INSERT INTO images (fingerprint, filename, size, public, auto_update, 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)
+	result, err := stmt.Exec(fp, fname, sz, publicInt, autoUpdateInt, arch, creationDate, expiryDate)
 	if err != nil {
 		tx.Rollback()
 		return err
diff --git a/lxd/db_update.go b/lxd/db_update.go
index 6d27666..9f88ba8 100644
--- a/lxd/db_update.go
+++ b/lxd/db_update.go
@@ -15,6 +15,23 @@ import (
 	log "gopkg.in/inconshreveable/log15.v2"
 )
 
+func dbUpdateFromV26(db *sql.DB) error {
+	stmt := `
+ALTER TABLE images ADD COLUMN auto_update INTEGER NOT NULL DEFAULT 0;
+CREATE TABLE IF NOT EXISTS images_source (
+    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+    image_id INTEGER NOT NULL,
+    server TEXT NOT NULL,
+    protocol INTEGER NOT NULL,
+    certificate TEXT NOT NULL,
+    alias VARCHAR(255) NOT NULL,
+    FOREIGN KEY (image_id) REFERENCES images (id) ON DELETE CASCADE
+);
+INSERT INTO schema (version, updated_at) VALUES (?, strftime("%s"));`
+	_, err := db.Exec(stmt, 27)
+	return err
+}
+
 func dbUpdateFromV25(db *sql.DB) error {
 	stmt := `
 INSERT INTO profiles (name, description) VALUES ("docker", "Profile supporting docker in containers");
@@ -942,6 +959,12 @@ func dbUpdate(d *Daemon, prevVersion int) error {
 			return err
 		}
 	}
+	if prevVersion < 27 {
+		err = dbUpdateFromV26(db)
+		if err != nil {
+			return err
+		}
+	}
 
 	return nil
 }
diff --git a/lxd/images.go b/lxd/images.go
index 3cd3dd0..adf4a1d 100644
--- a/lxd/images.go
+++ b/lxd/images.go
@@ -154,6 +154,7 @@ type imagePostReq struct {
 	Public     bool              `json:"public"`
 	Source     map[string]string `json:"source"`
 	Properties map[string]string `json:"properties"`
+	AutoUpdate bool              `json:"auto_update"`
 }
 
 type imageMetadata struct {
@@ -272,27 +273,15 @@ func imgPostRemoteInfo(d *Daemon, req imagePostReq, op *operation) error {
 	var err error
 	var hash string
 
-	if req.Source["alias"] != "" {
-		if req.Source["mode"] == "pull" && req.Source["server"] != "" {
-			hash, err = remoteGetImageFingerprint(d, req.Source["server"], req.Source["certificate"], req.Source["alias"])
-			if err != nil {
-				return err
-			}
-		} else {
-			_, alias, err := dbImageAliasGet(d.db, req.Source["alias"], true)
-			if err != nil {
-				return err
-			}
-
-			hash = alias.Target
-		}
-	} else if req.Source["fingerprint"] != "" {
+	if req.Source["fingerprint"] != "" {
 		hash = req.Source["fingerprint"]
+	} else if req.Source["alias"] != "" {
+		hash = req.Source["alias"]
 	} else {
 		return fmt.Errorf("must specify one of alias or fingerprint for init from image")
 	}
 
-	hash, err = d.ImageDownload(op, req.Source["server"], req.Source["protocol"], req.Source["certificate"], req.Source["secret"], hash, false)
+	hash, err = d.ImageDownload(op, req.Source["server"], req.Source["protocol"], req.Source["certificate"], req.Source["secret"], hash, false, req.AutoUpdate)
 	if err != nil {
 		return err
 	}
@@ -308,8 +297,8 @@ func imgPostRemoteInfo(d *Daemon, req imagePostReq, op *operation) error {
 	}
 
 	// 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 req.Public || req.AutoUpdate || req.Filename != "" || len(req.Properties) > 0 {
+		err = dbImageUpdate(d.db, id, req.Filename, info.Size, req.Public, req.AutoUpdate, info.Architecture, info.CreationDate, info.ExpiryDate, info.Properties)
 		if err != nil {
 			return err
 		}
@@ -376,7 +365,7 @@ func imgPostURLInfo(d *Daemon, req imagePostReq, op *operation) error {
 	}
 
 	// Import the image
-	hash, err = d.ImageDownload(op, url, "direct", "", "", hash, false)
+	hash, err = d.ImageDownload(op, url, "direct", "", "", hash, false, req.AutoUpdate)
 	if err != nil {
 		return err
 	}
@@ -391,8 +380,8 @@ func imgPostURLInfo(d *Daemon, req imagePostReq, op *operation) error {
 		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 req.Public || req.AutoUpdate || req.Filename != "" || len(req.Properties) > 0 {
+		err = dbImageUpdate(d.db, id, req.Filename, info.Size, req.Public, req.AutoUpdate, info.Architecture, info.CreationDate, info.ExpiryDate, info.Properties)
 		if err != nil {
 			return err
 		}
@@ -610,6 +599,7 @@ func imageBuildFromInfo(d *Daemon, info shared.ImageInfo) (metadata map[string]s
 		info.Filename,
 		info.Size,
 		info.Public,
+		info.AutoUpdate,
 		info.Architecture,
 		info.CreationDate,
 		info.ExpiryDate,
@@ -805,6 +795,96 @@ func imagesGet(d *Daemon, r *http.Request) Response {
 
 var imagesCmd = Command{name: "images", post: imagesPost, untrustedGet: true, get: imagesGet}
 
+func autoUpdateImages(d *Daemon) {
+	shared.Debugf("Updating images")
+
+	images, err := dbImagesGet(d.db, false)
+	if err != nil {
+		shared.Log.Error("Unable to retrieve the list of images", log.Ctx{"err": err})
+		return
+	}
+
+	for _, fp := range images {
+		id, info, err := dbImageGet(d.db, fp, false, true)
+		if err != nil {
+			shared.Log.Error("Error loading image", log.Ctx{"err": err, "fp": fp})
+			continue
+		}
+
+		if !info.AutoUpdate {
+			continue
+		}
+
+		_, source, err := dbImageSourceGet(d.db, id)
+		if err != nil {
+			continue
+		}
+
+		shared.Log.Debug("Processing image", log.Ctx{"fp": fp, "server": source.Server, "protocol": source.Protocol, "alias": source.Alias})
+
+		hash, err := d.ImageDownload(nil, source.Server, source.Protocol, "", "", source.Alias, false, true)
+		if hash == fp {
+			shared.Log.Debug("Already up to date", log.Ctx{"fp": fp})
+			continue
+		}
+
+		newId, _, err := dbImageGet(d.db, hash, false, true)
+		if err != nil {
+			shared.Log.Error("Error loading image", log.Ctx{"err": err, "fp": hash})
+			continue
+		}
+
+		err = dbImageLastAccessUpdate(d.db, hash, info.LastUsedDate)
+		if err != nil {
+			shared.Log.Error("Error setting last use date", log.Ctx{"err": err, "fp": hash})
+			continue
+		}
+
+		err = dbImageAliasesMove(d.db, id, newId)
+		if err != nil {
+			shared.Log.Error("Error moving aliases", log.Ctx{"err": err, "fp": hash})
+			continue
+		}
+
+		err = doDeleteImage(d, fp)
+		if err != nil {
+			shared.Log.Error("Error deleting image", log.Ctx{"err": err, "fp": fp})
+		}
+	}
+}
+
+func pruneExpiredImages(d *Daemon) {
+	shared.Debugf("Pruning expired images")
+	expiry, err := d.ConfigValueGet("images.remote_cache_expiry")
+	if err != nil {
+		shared.Log.Error("Unable to read the images.remote_cache_expiry key")
+		return
+	}
+
+	if expiry == "" {
+		expiry = "10"
+	}
+
+	expiryInt, err := strconv.Atoi(expiry)
+	if err != nil {
+		shared.Log.Error("Invalid value for images.remote_cache_expiry", log.Ctx{"err": err})
+		return
+	}
+
+	images, err := dbImagesGetExpired(d.db, expiryInt)
+	if err != nil {
+		shared.Log.Error("Unable to retrieve the list of expired images", log.Ctx{"err": err})
+		return
+	}
+
+	for _, fp := range images {
+		if err := doDeleteImage(d, fp); err != nil {
+			shared.Log.Error("Error deleting image", log.Ctx{"err": err, "fp": fp})
+		}
+	}
+	shared.Debugf("Done pruning expired images")
+}
+
 func doDeleteImage(d *Daemon, fingerprint string) error {
 	id, imgInfo, err := dbImageGet(d.db, fingerprint, false, false)
 	if err != nil {
@@ -918,6 +998,7 @@ func imageGet(d *Daemon, r *http.Request) Response {
 type imagePutReq struct {
 	Properties map[string]string `json:"properties"`
 	Public     bool              `json:"public"`
+	AutoUpdate bool              `json:"auto_update"`
 }
 
 func imagePut(d *Daemon, r *http.Request) Response {
@@ -933,7 +1014,7 @@ func imagePut(d *Daemon, r *http.Request) Response {
 		return SmartError(err)
 	}
 
-	err = dbImageUpdate(d.db, id, info.Filename, info.Size, req.Public, info.Architecture, info.CreationDate, info.ExpiryDate, req.Properties)
+	err = dbImageUpdate(d.db, id, info.Filename, info.Size, req.Public, req.AutoUpdate, info.Architecture, info.CreationDate, info.ExpiryDate, req.Properties)
 	if err != nil {
 		return SmartError(err)
 	}
diff --git a/po/lxd.pot b/po/lxd.pot
index 87e1ae0..d4a034e 100644
--- a/po/lxd.pot
+++ b/po/lxd.pot
@@ -7,7 +7,7 @@
 msgid   ""
 msgstr  "Project-Id-Version: lxd\n"
         "Report-Msgid-Bugs-To: lxc-devel at lists.linuxcontainers.org\n"
-        "POT-Creation-Date: 2016-03-01 00:43-0500\n"
+        "POT-Creation-Date: 2016-03-01 17:59-0500\n"
         "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
         "Last-Translator: FULL NAME <EMAIL at ADDRESS>\n"
         "Language-Team: LANGUAGE <LL at li.org>\n"
@@ -36,7 +36,7 @@ msgid   "### This is a yaml representation of the configuration.\n"
         "### Note that the name is shown but cannot be changed"
 msgstr  ""
 
-#: lxc/image.go:82
+#: lxc/image.go:83
 msgid   "### This is a yaml representation of the image properties.\n"
         "### Any line starting with a '# will be ignored.\n"
         "###\n"
@@ -65,7 +65,7 @@ msgid   "### This is a yaml representation of the profile.\n"
         "### Note that the name is shown but cannot be changed"
 msgstr  ""
 
-#: lxc/image.go:541
+#: lxc/image.go:558
 #, c-format
 msgid   "%s (%d more)"
 msgstr  ""
@@ -78,11 +78,11 @@ msgstr  ""
 msgid   "(none)"
 msgstr  ""
 
-#: lxc/image.go:561 lxc/image.go:585
+#: lxc/image.go:578 lxc/image.go:602
 msgid   "ALIAS"
 msgstr  ""
 
-#: lxc/image.go:565
+#: lxc/image.go:582
 msgid   "ARCH"
 msgstr  ""
 
@@ -99,7 +99,7 @@ msgstr  ""
 msgid   "Admin password for %s: "
 msgstr  ""
 
-#: lxc/image.go:314
+#: lxc/image.go:324
 msgid   "Aliases:"
 msgstr  ""
 
@@ -107,11 +107,16 @@ msgstr  ""
 msgid   "An environment variable of the form HOME=/home/foo"
 msgstr  ""
 
-#: lxc/image.go:297 lxc/info.go:87
+#: lxc/image.go:307 lxc/info.go:87
 #, c-format
 msgid   "Architecture: %s"
 msgstr  ""
 
+#: lxc/image.go:328
+#, c-format
+msgid   "Auto update: %s"
+msgstr  ""
+
 #: lxc/help.go:49
 msgid   "Available commands:"
 msgstr  ""
@@ -162,7 +167,7 @@ msgstr  ""
 msgid   "Config key/value to apply to the new container"
 msgstr  ""
 
-#: lxc/config.go:492 lxc/config.go:557 lxc/image.go:639 lxc/profile.go:187
+#: lxc/config.go:492 lxc/config.go:557 lxc/image.go:656 lxc/profile.go:187
 #, c-format
 msgid   "Config parsing error: %s"
 msgstr  ""
@@ -185,7 +190,7 @@ msgstr  ""
 msgid   "Container published with fingerprint: %s"
 msgstr  ""
 
-#: lxc/image.go:151
+#: lxc/image.go:155
 msgid   "Copy aliases from source"
 msgstr  ""
 
@@ -195,7 +200,7 @@ msgid   "Copy containers within or in between lxd instances.\n"
         "lxc copy [remote:]<source container> [remote:]<destination container> [--ephemeral|e]"
 msgstr  ""
 
-#: lxc/image.go:245
+#: lxc/image.go:250
 #, c-format
 msgid   "Copying the image: %s"
 msgstr  ""
@@ -220,7 +225,7 @@ msgid   "Create a read-only snapshot of a container.\n"
         "lxc snapshot u1 snap0"
 msgstr  ""
 
-#: lxc/image.go:302 lxc/info.go:89
+#: lxc/image.go:312 lxc/info.go:89
 #, c-format
 msgid   "Created: %s"
 msgstr  ""
@@ -234,7 +239,7 @@ msgstr  ""
 msgid   "Creating the container"
 msgstr  ""
 
-#: lxc/image.go:564 lxc/image.go:587
+#: lxc/image.go:581 lxc/image.go:604
 msgid   "DESCRIPTION"
 msgstr  ""
 
@@ -292,16 +297,16 @@ msgid   "Execute the specified command in a container.\n"
         "Mode defaults to non-interactive, interactive mode is selected if both stdin AND stdout are terminals (stderr is ignored)."
 msgstr  ""
 
-#: lxc/image.go:306
+#: lxc/image.go:316
 #, c-format
 msgid   "Expires: %s"
 msgstr  ""
 
-#: lxc/image.go:308
+#: lxc/image.go:318
 msgid   "Expires: never"
 msgstr  ""
 
-#: lxc/config.go:267 lxc/image.go:562 lxc/image.go:586
+#: lxc/config.go:267 lxc/image.go:579 lxc/image.go:603
 msgid   "FINGERPRINT"
 msgstr  ""
 
@@ -309,7 +314,7 @@ msgstr  ""
 msgid   "Fast mode (same as --columns=nsacPt"
 msgstr  ""
 
-#: lxc/image.go:289
+#: lxc/image.go:305
 #, c-format
 msgid   "Fingerprint: %s"
 msgstr  ""
@@ -364,11 +369,11 @@ msgstr  ""
 msgid   "Ignore the container state (only forstart)."
 msgstr  ""
 
-#: lxc/image.go:250
+#: lxc/image.go:255
 msgid   "Image copied successfully!"
 msgstr  ""
 
-#: lxc/image.go:379
+#: lxc/image.go:396
 #, c-format
 msgid   "Image imported with fingerprint: %s"
 msgstr  ""
@@ -405,6 +410,10 @@ msgstr  ""
 msgid   "Ips:"
 msgstr  ""
 
+#: lxc/image.go:156
+msgid   "Keep the image up to date after initial copy"
+msgstr  ""
+
 #: lxc/main.go:35
 msgid   "LXD socket not found; is LXD running?"
 msgstr  ""
@@ -463,7 +472,7 @@ msgstr  ""
 msgid   "Log:"
 msgstr  ""
 
-#: lxc/image.go:150
+#: lxc/image.go:154
 msgid   "Make image public"
 msgstr  ""
 
@@ -562,7 +571,7 @@ msgid   "Manage remote LXD servers.\n"
         "lxc remote get-default                                                                 Print the default remote."
 msgstr  ""
 
-#: lxc/image.go:92
+#: lxc/image.go:93
 msgid   "Manipulate container images.\n"
         "\n"
         "In LXD containers are created from images. Those images were themselves\n"
@@ -583,9 +592,12 @@ msgid   "Manipulate container images.\n"
         "lxc image import <tarball> [rootfs tarball|URL] [remote:] [--public] [--created-at=ISO-8601] [--expires-at=ISO-8601] [--fingerprint=FINGERPRINT] [prop=value]\n"
         "    Import an image tarball (or tarballs) into the LXD image store.\n"
         "\n"
-        "lxc image copy [remote:]<image> <remote>: [--alias=ALIAS].. [--copy-aliases] [--public]\n"
+        "lxc image copy [remote:]<image> <remote>: [--alias=ALIAS].. [--copy-aliases] [--public] [--auto-update]\n"
         "    Copy an image from one LXD daemon to another over the network.\n"
         "\n"
+        "    The auto-update flag instructs the server to keep this image up to\n"
+        "    date. It requires the source to be an alias and for it to be public.\n"
+        "\n"
         "lxc image delete [remote:]<image>\n"
         "    Delete an image from the LXD image store.\n"
         "\n"
@@ -663,7 +675,7 @@ msgstr  ""
 msgid   "Name: %s"
 msgstr  ""
 
-#: lxc/image.go:152 lxc/publish.go:33
+#: lxc/image.go:157 lxc/publish.go:33
 msgid   "New alias to define at target"
 msgstr  ""
 
@@ -675,7 +687,7 @@ msgstr  ""
 msgid   "No fingerprint specified."
 msgstr  ""
 
-#: lxc/image.go:371
+#: lxc/image.go:388
 msgid   "Only https:// is supported for remote image import."
 msgstr  ""
 
@@ -683,7 +695,7 @@ msgstr  ""
 msgid   "Options:"
 msgstr  ""
 
-#: lxc/image.go:466
+#: lxc/image.go:483
 #, c-format
 msgid   "Output is in %s"
 msgstr  ""
@@ -708,7 +720,7 @@ msgstr  ""
 msgid   "PROTOCOL"
 msgstr  ""
 
-#: lxc/image.go:563 lxc/remote.go:315
+#: lxc/image.go:580 lxc/remote.go:315
 msgid   "PUBLIC"
 msgstr  ""
 
@@ -739,7 +751,7 @@ msgstr  ""
 msgid   "Press enter to open the editor again"
 msgstr  ""
 
-#: lxc/config.go:493 lxc/config.go:558 lxc/image.go:640
+#: lxc/config.go:493 lxc/config.go:558 lxc/image.go:657
 msgid   "Press enter to start the editor again"
 msgstr  ""
 
@@ -790,7 +802,7 @@ msgstr  ""
 msgid   "Profiles: %s"
 msgstr  ""
 
-#: lxc/image.go:310
+#: lxc/image.go:320
 msgid   "Properties:"
 msgstr  ""
 
@@ -798,7 +810,7 @@ msgstr  ""
 msgid   "Public image server"
 msgstr  ""
 
-#: lxc/image.go:298
+#: lxc/image.go:308
 #, c-format
 msgid   "Public: %s"
 msgstr  ""
@@ -827,7 +839,7 @@ msgstr  ""
 msgid   "Retrieving image: %s"
 msgstr  ""
 
-#: lxc/image.go:566
+#: lxc/image.go:583
 msgid   "SIZE"
 msgstr  ""
 
@@ -884,7 +896,7 @@ msgstr  ""
 msgid   "Show the container's last 100 log lines?"
 msgstr  ""
 
-#: lxc/image.go:296
+#: lxc/image.go:306
 #, c-format
 msgid   "Size: %.2fMB"
 msgstr  ""
@@ -893,6 +905,10 @@ msgstr  ""
 msgid   "Snapshots:"
 msgstr  ""
 
+#: lxc/image.go:330
+msgid   "Source:"
+msgstr  ""
+
 #: lxc/launch.go:122
 #, c-format
 msgid   "Starting %s"
@@ -935,11 +951,11 @@ msgstr  ""
 msgid   "Time to wait for the container before killing it."
 msgstr  ""
 
-#: lxc/image.go:299
+#: lxc/image.go:309
 msgid   "Timestamps:"
 msgstr  ""
 
-#: lxc/image.go:362
+#: lxc/image.go:379
 #, c-format
 msgid   "Transfering image: %d%%"
 msgstr  ""
@@ -957,7 +973,7 @@ msgstr  ""
 msgid   "Type: persistent"
 msgstr  ""
 
-#: lxc/image.go:567
+#: lxc/image.go:584
 msgid   "UPLOAD DATE"
 msgstr  ""
 
@@ -965,7 +981,7 @@ msgstr  ""
 msgid   "URL"
 msgstr  ""
 
-#: lxc/image.go:304
+#: lxc/image.go:314
 #, c-format
 msgid   "Uploaded: %s"
 msgstr  ""
@@ -1027,6 +1043,14 @@ msgstr  ""
 msgid   "didn't get any affected image, container or snapshot from server"
 msgstr  ""
 
+#: lxc/image.go:300
+msgid   "disabled"
+msgstr  ""
+
+#: lxc/image.go:302
+msgid   "enabled"
+msgstr  ""
+
 #: lxc/main.go:25 lxc/main.go:157
 #, c-format
 msgid   "error: %v"
@@ -1041,7 +1065,7 @@ msgstr  ""
 msgid   "got bad version"
 msgstr  ""
 
-#: lxc/image.go:290 lxc/image.go:544
+#: lxc/image.go:295 lxc/image.go:561
 msgid   "no"
 msgstr  ""
 
@@ -1099,7 +1123,7 @@ msgstr  ""
 msgid   "wrong number of subcommand arguments"
 msgstr  ""
 
-#: lxc/delete.go:45 lxc/image.go:293 lxc/image.go:548
+#: lxc/delete.go:45 lxc/image.go:297 lxc/image.go:565
 msgid   "yes"
 msgstr  ""
 
diff --git a/shared/image.go b/shared/image.go
index c2feaaa..e2e39d4 100644
--- a/shared/image.go
+++ b/shared/image.go
@@ -19,19 +19,30 @@ type ImageAlias struct {
 	Description string `json:"description"`
 }
 
+type ImageSource struct {
+	Server      string `json:"server"`
+	Protocol    string `json:"protocol"`
+	Certificate string `json:"certificate"`
+	Alias       string `json:"alias"`
+}
+
 type ImageInfo struct {
 	Aliases      []ImageAlias      `json:"aliases"`
 	Architecture string            `json:"architecture"`
 	Cached       bool              `json:"cached"`
-	Fingerprint  string            `json:"fingerprint"`
 	Filename     string            `json:"filename"`
+	Fingerprint  string            `json:"fingerprint"`
 	Properties   map[string]string `json:"properties"`
 	Public       bool              `json:"public"`
 	Size         int64             `json:"size"`
-	CreationDate time.Time         `json:"created_at"`
-	ExpiryDate   time.Time         `json:"expires_at"`
-	LastUsedDate time.Time         `json:"last_used_at"`
-	UploadDate   time.Time         `json:"uploaded_at"`
+
+	AutoUpdate bool         `json:"auto_update"`
+	Source     *ImageSource `json:"update_source,omitempty"`
+
+	CreationDate time.Time `json:"created_at"`
+	ExpiryDate   time.Time `json:"expires_at"`
+	LastUsedDate time.Time `json:"last_used_at"`
+	UploadDate   time.Time `json:"uploaded_at"`
 }
 
 /*
@@ -39,12 +50,14 @@ type ImageInfo struct {
  * ImageInfo, namely those which a user may update
  */
 type BriefImageInfo struct {
+	AutoUpdate bool              `json:"auto_update"`
 	Properties map[string]string `json:"properties"`
 	Public     bool              `json:"public"`
 }
 
 func (i *ImageInfo) Brief() BriefImageInfo {
 	retstate := BriefImageInfo{
+		AutoUpdate: i.AutoUpdate,
 		Properties: i.Properties,
 		Public:     i.Public}
 	return retstate
diff --git a/specs/configuration.md b/specs/configuration.md
index fe7e6f7..efa0430 100644
--- a/specs/configuration.md
+++ b/specs/configuration.md
@@ -28,6 +28,8 @@ storage.lvm\_fstype             | string        | ext4                      | Fo
 storage.zfs\_pool\_name         | string        | -                         | ZFS pool name
 images.compression\_algorithm   | string        | gzip                      | Compression algorithm to use for new images (bzip2, gzip, lzma, xz or none)
 images.remote\_cache\_expiry    | integer       | 10                        | Number of days after which an unused cached remote image will be flushed
+images.auto\_update\_interval   | integer       | 6                         | Interval in hours at which to look for update to cached images (0 disables it)
+images.auto\_update\_cached     | boolean       | true                      | Whether to automatically update any image that LXD caches
 
 Those keys can be set using the lxc tool with:
 
diff --git a/specs/database.md b/specs/database.md
index 1c3a703..fd7de4f 100644
--- a/specs/database.md
+++ b/specs/database.md
@@ -57,6 +57,7 @@ The list of tables is:
  * images
  * images\_properties
  * images\_aliases
+ * images\_source
  * profiles
  * profiles\_config
  * profiles\_devices
@@ -182,6 +183,7 @@ fingerprint     | VARCHAR(255)  | -             | NOT NULL          | Tarball fi
 filename        | VARCHAR(255)  | -             | NOT NULL          | Tarball filename
 size            | INTEGER       | -             | NOT NULL          | Tarball size
 public          | INTEGER       | 0             | NOT NULL          | Whether the image is public or not
+auto\_update    | INTEGER       | 0             | NOT NULL          | Whether to update from the source of this image
 architecture    | INTEGER       | -             | NOT NULL          | Image architecture
 creation\_date  | DATETIME      | -             |                   | Image creation date (user supplied, 0 = unknown)
 expiry\_date    | DATETIME      | -             |                   | Image expiry (user supplied, 0 = never)
@@ -219,6 +221,20 @@ Index: UNIQUE ON id
 
 Foreign keys: image\_id REFERENCES images(id)
 
+## images\_source
+
+Column          | Type          | Default       | Constraint        | Description
+:-----          | :---          | :------       | :---------        | :----------
+id              | INTEGER       | SERIAL        | NOT NULL          | SERIAL
+image\_id       | INTEGER       | -             | NOT NULL          | images.id FK
+server          | TEXT          | -             | NOT NULL          | Server URL
+protocol        | INTEGER       | 0             | NOT NULL          | Protocol to access the remote (0 = lxd, 1 = direct, 2 = simplestreams)
+alias           | VARCHAR(255)  | -             | NOT NULL          | What remote alias to use as the source
+certificate     | TEXT          | -             |                   | PEM encoded certificate of the server
+
+Index: UNIQUE ON id
+
+Foreign keys: image\_id REFERENCES images(id)
 
 ## profiles
 
diff --git a/specs/rest-api.md b/specs/rest-api.md
index 034f9df..3d9f314 100644
--- a/specs/rest-api.md
+++ b/specs/rest-api.md
@@ -295,7 +295,7 @@ 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
+        "certificate": "PEM certificate",       # 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)
     }
@@ -1105,6 +1105,7 @@ Output:
             }
         ],
         "architecture": "x86_64",
+        "auto_update": true,
         "cached": false,
         "fingerprint": "54c8caac1f61901ed86c68f24af5f5d3672bdc62c71d04f06df3a59e95684473",
         "filename": "ubuntu-trusty-14.04-amd64-server-20160201.tar.xz",
@@ -1114,6 +1115,12 @@ Output:
             "os": "ubuntu",
             "release": "trusty"
         },
+        "update_source": {
+            "server": "https://10.1.2.4:8443",
+            "protocol": "lxd",
+            "certificate": "PEM certificate",
+            "alias": "ubuntu/trusty/amd64"
+        },
         "public": false,
         "size": 123792592,
         "created_at": "2016-02-01T21:07:41Z",
@@ -1144,6 +1151,7 @@ HTTP code for this should be 202 (Accepted).
 Input:
 
     {
+        "auto_update": true,
         "properties": {
             "architecture": "x86_64",
             "description": "Ubuntu 14.04 LTS server (20160201)",
diff --git a/test/suites/database_update.sh b/test/suites/database_update.sh
index bb37f79..d89d757 100644
--- a/test/suites/database_update.sh
+++ b/test/suites/database_update.sh
@@ -11,12 +11,12 @@ test_database_update(){
   spawn_lxd "${LXD_MIGRATE_DIR}"
 
   # Assert there are enough tables.
-  expected_tables=15
+  expected_tables=16
   tables=$(sqlite3 "${MIGRATE_DB}" ".dump" | grep -c "CREATE TABLE")
   [ "${tables}" -eq "${expected_tables}" ] || { echo "FAIL: Wrong number of tables after database migration. Found: ${tables}, expected ${expected_tables}"; false; }
 
   # There should be 10 "ON DELETE CASCADE" occurences
-  expected_cascades=10
+  expected_cascades=11
   cascades=$(sqlite3 "${MIGRATE_DB}" ".dump" | grep -c "ON DELETE CASCADE")
   [ "${cascades}" -eq "${expected_cascades}" ] || { echo "FAIL: Wrong number of ON DELETE CASCADE foreign keys. Found: ${cascades}, exected: ${expected_cascades}"; false; }
 }


More information about the lxc-devel mailing list