[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