diff --git a/README.md b/README.md index 3260c65070e0..e88cdd7bd143 100644 --- a/README.md +++ b/README.md @@ -85,18 +85,13 @@ group to talk to LXD; you can create your own group if you want): LXD has two parts, the daemon (the `lxd` binary), and the client (the `lxc` binary). Now that the daemon is all configured and running (either via the -packaging or via the from-source instructions above), you can import an image: +packaging or via the from-source instructions above), you can create a container: - $GOPATH/src/github.com/lxc/lxd/scripts/lxd-images import ubuntu --alias ubuntu - -With that image imported into LXD, you can now start containers: - - $GOPATH/bin/lxc launch ubuntu + $GOPATH/bin/lxc launch ubuntu:14.04 Alternatively, you can also use a remote LXD host as a source of images. -Those will be automatically cached for you for up at container startup time: +One comes pre-configured in LXD, called "images" (images.linuxcontainers.org) - $GOPATH/bin/lxc remote add images images.linuxcontainers.org $GOPATH/bin/lxc launch images:centos/7/amd64 centos ## Bug reports diff --git a/client.go b/client.go index 29e5d750e544..b2f05128e0be 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{} @@ -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 { @@ -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 } @@ -1173,7 +1169,7 @@ func (c *Client) Init(name string, imgremote string, image string, profiles *[]s source := shared.Jmap{"type": "image"} if image == "" { - return nil, fmt.Errorf("You must provide an image hash or alias name.") + image = "default" } if imgremote != c.Name { @@ -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) { diff --git a/lxc/config.go b/lxc/config.go index 5f02ee1493e4..7bf88f06b950 100644 --- a/lxc/config.go +++ b/lxc/config.go @@ -262,6 +262,7 @@ func (c *configCmd) run(config *lxd.Config, args []string) error { table := tablewriter.NewWriter(os.Stdout) table.SetAutoWrapText(false) + table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetRowLine(true) table.SetHeader([]string{ i18n.G("FINGERPRINT"), diff --git a/lxc/image.go b/lxc/image.go index a4e3745fb807..c411e37ca230 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 [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:] : [--alias=ALIAS].. [--copy-aliases] [--public] +lxc image copy [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:] 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")) } @@ -224,18 +229,22 @@ func (c *imageCmd) run(config *lxd.Config, args []string) error { if len(args) != 3 { return errArgs } + remote, inName := config.ParseRemoteAndContainer(args[1]) if inName == "" { - return errArgs + inName = "default" } + destRemote, outName := config.ParseRemoteAndContainer(args[2]) if outName != "" { return errArgs } + d, err := lxd.NewClient(config, remote) if err != nil { return err } + dest, err := lxd.NewClient(config, destRemote) if err != nil { return err @@ -245,7 +254,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!")) } @@ -256,14 +265,17 @@ func (c *imageCmd) run(config *lxd.Config, args []string) error { if len(args) < 2 { return errArgs } + remote, inName := config.ParseRemoteAndContainer(args[1]) if inName == "" { - return errArgs + inName = "default" } + d, err := lxd.NewClient(config, remote) if err != nil { return err } + image := c.dereferenceAlias(d, inName) err = d.DeleteImage(image) return err @@ -272,10 +284,12 @@ func (c *imageCmd) run(config *lxd.Config, args []string) error { if len(args) < 2 { return errArgs } + remote, inName := config.ParseRemoteAndContainer(args[1]) if inName == "" { - return errArgs + inName = "default" } + d, err := lxd.NewClient(config, remote) if err != nil { return err @@ -286,13 +300,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 +334,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": @@ -420,7 +446,7 @@ func (c *imageCmd) run(config *lxd.Config, args []string) error { remote, inName := config.ParseRemoteAndContainer(args[1]) if inName == "" { - return errArgs + inName = "default" } d, err := lxd.NewClient(config, remote) @@ -442,7 +468,7 @@ func (c *imageCmd) run(config *lxd.Config, args []string) error { remote, inName := config.ParseRemoteAndContainer(args[1]) if inName == "" { - return errArgs + inName = "default" } d, err := lxd.NewClient(config, remote) @@ -471,10 +497,12 @@ func (c *imageCmd) run(config *lxd.Config, args []string) error { if len(args) < 2 { return errArgs } + remote, inName := config.ParseRemoteAndContainer(args[1]) if inName == "" { - return errArgs + inName = "default" } + d, err := lxd.NewClient(config, remote) if err != nil { return err @@ -556,6 +584,7 @@ func (c *imageCmd) showImages(images []shared.ImageInfo, filters []string) error table := tablewriter.NewWriter(os.Stdout) table.SetAutoWrapText(false) + table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetRowLine(true) table.SetHeader([]string{ i18n.G("ALIAS"), @@ -580,6 +609,7 @@ func (c *imageCmd) showAliases(aliases shared.ImageAliases) error { table := tablewriter.NewWriter(os.Stdout) table.SetAutoWrapText(false) + table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetRowLine(true) table.SetHeader([]string{ i18n.G("ALIAS"), diff --git a/lxc/init.go b/lxc/init.go index 96830cd026a3..27446f581a2e 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) } diff --git a/lxc/list.go b/lxc/list.go index dfa85df2d93d..456bee0dfb2b 100644 --- a/lxc/list.go +++ b/lxc/list.go @@ -277,6 +277,7 @@ func (c *listCmd) listContainers(d *lxd.Client, cinfos []shared.ContainerInfo, f table := tablewriter.NewWriter(os.Stdout) table.SetAutoWrapText(false) + table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetRowLine(true) table.SetHeader(headers) sort.Sort(byName(data)) diff --git a/lxc/main.go b/lxc/main.go index a25cafe2372b..ec43c214c214 100644 --- a/lxc/main.go +++ b/lxc/main.go @@ -141,9 +141,6 @@ func run() error { if err != nil { return err } - - fmt.Fprintf(os.Stderr, i18n.G("If this is your first run, you will need to import images using the 'lxd-images' script.")+"\n") - fmt.Fprintf(os.Stderr, i18n.G("For example: 'lxd-images import ubuntu --alias ubuntu'.")+"\n") } err = cmd.run(config, gnuflag.Args()) diff --git a/lxc/publish.go b/lxc/publish.go index 690dfdfb5173..b09675cf4e5d 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/lxc/remote.go b/lxc/remote.go index 0e1cde7904e0..0e5c502f40c0 100644 --- a/lxc/remote.go +++ b/lxc/remote.go @@ -27,6 +27,7 @@ type remoteCmd struct { acceptCert bool password string public bool + protocol string } func (c *remoteCmd) showByDefault() bool { @@ -37,26 +38,39 @@ func (c *remoteCmd) usage() string { return i18n.G( `Manage remote LXD servers. -lxc remote add [--accept-certificate] [--password=PASSWORD] [--public] Add the remote at . -lxc remote remove Remove the remote . -lxc remote list List all remotes. -lxc remote rename Rename remote to . -lxc remote set-url Update 's url to . -lxc remote set-default Set the default remote. -lxc remote get-default Print the default remote.`) +lxc remote add [--accept-certificate] [--password=PASSWORD] + [--public] [--protocol=PROTOCOL] Add the remote at . +lxc remote remove Remove the remote . +lxc remote list List all remotes. +lxc remote rename Rename remote to . +lxc remote set-url Update 's url to . +lxc remote set-default Set the default remote. +lxc remote get-default Print the default remote.`) } func (c *remoteCmd) flags() { gnuflag.BoolVar(&c.acceptCert, "accept-certificate", false, i18n.G("Accept certificate")) gnuflag.StringVar(&c.password, "password", "", i18n.G("Remote admin password")) + gnuflag.StringVar(&c.protocol, "protocol", "", i18n.G("Server protocol (lxd or simplestreams)")) gnuflag.BoolVar(&c.public, "public", false, i18n.G("Public image server")) } -func (c *remoteCmd) addServer(config *lxd.Config, server string, addr string, acceptCert bool, password string, public bool) error { +func (c *remoteCmd) addServer(config *lxd.Config, server string, addr string, acceptCert bool, password string, public bool, protocol string) error { var rScheme string var rHost string var rPort string + // Setup the remotes list + if config.Remotes == nil { + config.Remotes = make(map[string]lxd.RemoteConfig) + } + + // Fast track simplestreams + if protocol == "simplestreams" { + config.Remotes[server] = lxd.RemoteConfig{Addr: addr, Public: true, Protocol: protocol} + return nil + } + /* Complex remote URL parsing */ remoteURL, err := url.Parse(addr) if err != nil { @@ -118,12 +132,8 @@ func (c *remoteCmd) addServer(config *lxd.Config, server string, addr string, ac addr = rScheme + "://" + rHost } - if config.Remotes == nil { - config.Remotes = make(map[string]lxd.RemoteConfig) - } - /* Actually add the remote */ - config.Remotes[server] = lxd.RemoteConfig{Addr: addr} + config.Remotes[server] = lxd.RemoteConfig{Addr: addr, Protocol: protocol} remote := config.ParseRemote(server) d, err := lxd.NewClient(config, remote) @@ -252,7 +262,7 @@ func (c *remoteCmd) run(config *lxd.Config, args []string) error { return fmt.Errorf(i18n.G("remote %s exists as <%s>"), args[1], rc.Addr) } - err := c.addServer(config, args[1], args[2], c.acceptCert, c.password, c.public) + err := c.addServer(config, args[1], args[2], c.acceptCert, c.password, c.public, c.protocol) if err != nil { delete(config.Remotes, args[1]) c.removeCertificate(config, args[1]) @@ -307,6 +317,7 @@ func (c *remoteCmd) run(config *lxd.Config, args []string) error { table := tablewriter.NewWriter(os.Stdout) table.SetAutoWrapText(false) + table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetRowLine(true) table.SetHeader([]string{ i18n.G("NAME"), diff --git a/lxd/container.go b/lxd/container.go index 99f6723f22da..4d7638eafdb3 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 b0f12d14a9a7..11a01d5a564d 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 49a911be6ea0..83a3686aa764 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 9773143c9b11..43efa77c6417 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 7bed7f479642..75dfae5ba44f 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 c845b4679ef8..efaefd4abcbc 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 6d27666f2313..9f88ba8d664a 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 3cd3dd0aae8b..adf4a1dc36aa 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 086a0bbbcc1f..a9d570c364d3 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@lists.linuxcontainers.org\n" - "POT-Creation-Date: 2016-03-02 14:02-0700\n" + "POT-Creation-Date: 2016-03-02 16:32-0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \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:569 #, c-format msgid "%s (%d more)" msgstr "" @@ -78,28 +78,28 @@ msgstr "" msgid "(none)" msgstr "" -#: lxc/image.go:561 lxc/image.go:585 +#: lxc/image.go:590 lxc/image.go:615 msgid "ALIAS" msgstr "" -#: lxc/image.go:565 +#: lxc/image.go:594 msgid "ARCH" msgstr "" -#: lxc/list.go:334 +#: lxc/list.go:335 msgid "ARCHITECTURE" msgstr "" -#: lxc/remote.go:50 +#: lxc/remote.go:52 msgid "Accept certificate" msgstr "" -#: lxc/remote.go:206 +#: lxc/remote.go:216 #, c-format msgid "Admin password for %s: " msgstr "" -#: lxc/image.go:314 +#: lxc/image.go:333 msgid "Aliases:" msgstr "" @@ -107,20 +107,25 @@ msgstr "" msgid "An environment variable of the form HOME=/home/foo" msgstr "" -#: lxc/image.go:297 lxc/info.go:87 +#: lxc/image.go:316 lxc/info.go:87 #, c-format msgid "Architecture: %s" msgstr "" +#: lxc/image.go:337 +#, c-format +msgid "Auto update: %s" +msgstr "" + #: lxc/help.go:49 msgid "Available commands:" msgstr "" -#: lxc/config.go:268 +#: lxc/config.go:269 msgid "COMMON NAME" msgstr "" -#: lxc/list.go:335 +#: lxc/list.go:336 msgid "CREATED AT" msgstr "" @@ -138,7 +143,7 @@ msgstr "" msgid "Cannot provide container name to list" msgstr "" -#: lxc/remote.go:156 +#: lxc/remote.go:166 #, c-format msgid "Certificate fingerprint: %x" msgstr "" @@ -150,7 +155,7 @@ msgid "Changes state of one or more containers to %s.\n" "lxc %s [...]" msgstr "" -#: lxc/remote.go:229 +#: lxc/remote.go:239 msgid "Client certificate stored at server: " 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:493 lxc/config.go:558 lxc/image.go:669 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,12 +200,12 @@ msgid "Copy containers within or in between lxd instances.\n" "lxc copy [remote:] [remote:] [--ephemeral|e]" msgstr "" -#: lxc/image.go:245 +#: lxc/image.go:254 #, c-format msgid "Copying the image: %s" msgstr "" -#: lxc/remote.go:171 +#: lxc/remote.go:181 msgid "Could not create server cert dir" 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:321 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:593 lxc/image.go:617 msgid "DESCRIPTION" msgstr "" @@ -246,21 +251,21 @@ msgid "Delete containers or container snapshots.\n" "Destroy containers or snapshots with any attached data (configuration, snapshots, ...)." msgstr "" -#: lxc/config.go:605 +#: lxc/config.go:606 #, c-format msgid "Device %s added to %s" msgstr "" -#: lxc/config.go:633 +#: lxc/config.go:634 #, c-format msgid "Device %s removed from %s" msgstr "" -#: lxc/list.go:418 +#: lxc/list.go:419 msgid "EPHEMERAL" msgstr "" -#: lxc/config.go:270 +#: lxc/config.go:271 msgid "EXPIRY DATE" 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:325 #, c-format msgid "Expires: %s" msgstr "" -#: lxc/image.go:308 +#: lxc/image.go:327 msgid "Expires: never" msgstr "" -#: lxc/config.go:267 lxc/image.go:562 lxc/image.go:586 +#: lxc/config.go:268 lxc/image.go:591 lxc/image.go:616 msgid "FINGERPRINT" msgstr "" @@ -309,7 +314,7 @@ msgstr "" msgid "Fast mode (same as --columns=nsacPt" msgstr "" -#: lxc/image.go:289 +#: lxc/image.go:314 #, c-format msgid "Fingerprint: %s" msgstr "" @@ -320,10 +325,6 @@ msgid "Fingers the LXD instance to check if it is up and working.\n" "lxc finger " msgstr "" -#: lxc/main.go:146 -msgid "For example: 'lxd-images import ubuntu --alias ubuntu'." -msgstr "" - #: lxc/action.go:37 msgid "Force the container to shutdown." msgstr "" @@ -340,22 +341,18 @@ msgstr "" msgid "Generating a client certificate. This may take a minute..." msgstr "" -#: lxc/list.go:332 +#: lxc/list.go:333 msgid "IPV4" msgstr "" -#: lxc/list.go:333 +#: lxc/list.go:334 msgid "IPV6" msgstr "" -#: lxc/config.go:269 +#: lxc/config.go:270 msgid "ISSUE DATE" msgstr "" -#: lxc/main.go:145 -msgid "If this is your first run, you will need to import images using the 'lxd-images' script." -msgstr "" - #: lxc/main.go:57 msgid "Ignore aliases when determining what command to run." msgstr "" @@ -364,11 +361,11 @@ msgstr "" msgid "Ignore the container state (only forstart)." msgstr "" -#: lxc/image.go:250 +#: lxc/image.go:259 msgid "Image copied successfully!" msgstr "" -#: lxc/image.go:379 +#: lxc/image.go:405 #, c-format msgid "Image imported with fingerprint: %s" msgstr "" @@ -405,6 +402,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 +464,7 @@ msgstr "" msgid "Log:" msgstr "" -#: lxc/image.go:150 +#: lxc/image.go:154 msgid "Make image public" msgstr "" @@ -550,19 +551,20 @@ msgid "Manage files on a container.\n" " in the case of pull, in the case of push and in the case of edit are /" msgstr "" -#: lxc/remote.go:37 +#: lxc/remote.go:38 msgid "Manage remote LXD servers.\n" "\n" - "lxc remote add [--accept-certificate] [--password=PASSWORD] [--public] Add the remote at .\n" - "lxc remote remove Remove the remote .\n" - "lxc remote list List all remotes.\n" - "lxc remote rename Rename remote to .\n" - "lxc remote set-url Update 's url to .\n" - "lxc remote set-default Set the default remote.\n" - "lxc remote get-default Print the default remote." + "lxc remote add [--accept-certificate] [--password=PASSWORD]\n" + " [--public] [--protocol=PROTOCOL] Add the remote at .\n" + "lxc remote remove Remove the remote .\n" + "lxc remote list List all remotes.\n" + "lxc remote rename Rename remote to .\n" + "lxc remote set-url Update 's url to .\n" + "lxc remote set-default Set the default remote.\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 +585,12 @@ msgid "Manipulate container images.\n" "lxc image import [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:] : [--alias=ALIAS].. [--copy-aliases] [--public]\n" + "lxc image copy [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:]\n" " Delete an image from the LXD image store.\n" "\n" @@ -654,11 +659,11 @@ msgstr "" msgid "Must supply container name for: " msgstr "" -#: lxc/list.go:336 lxc/remote.go:312 +#: lxc/list.go:337 lxc/remote.go:323 msgid "NAME" msgstr "" -#: lxc/remote.go:287 lxc/remote.go:292 +#: lxc/remote.go:297 lxc/remote.go:302 msgid "NO" msgstr "" @@ -667,19 +672,19 @@ 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 "" -#: lxc/config.go:279 +#: lxc/config.go:280 msgid "No certificate provided to add" msgstr "" -#: lxc/config.go:302 +#: lxc/config.go:303 msgid "No fingerprint specified." msgstr "" -#: lxc/image.go:371 +#: lxc/image.go:397 msgid "Only https:// is supported for remote image import." msgstr "" @@ -687,7 +692,7 @@ msgstr "" msgid "Options:" msgstr "" -#: lxc/image.go:466 +#: lxc/image.go:492 #, c-format msgid "Output is in %s" msgstr "" @@ -696,23 +701,23 @@ msgstr "" msgid "Override the terminal mode (auto, interactive or non-interactive)" msgstr "" -#: lxc/list.go:420 +#: lxc/list.go:421 msgid "PERSISTENT" msgstr "" -#: lxc/list.go:337 +#: lxc/list.go:338 msgid "PID" msgstr "" -#: lxc/list.go:338 +#: lxc/list.go:339 msgid "PROFILES" msgstr "" -#: lxc/remote.go:314 +#: lxc/remote.go:325 msgid "PROTOCOL" msgstr "" -#: lxc/image.go:563 lxc/remote.go:315 +#: lxc/image.go:592 lxc/remote.go:326 msgid "PUBLIC" msgstr "" @@ -743,7 +748,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:494 lxc/config.go:559 lxc/image.go:670 msgid "Press enter to start the editor again" msgstr "" @@ -794,15 +799,15 @@ msgstr "" msgid "Profiles: %s" msgstr "" -#: lxc/image.go:310 +#: lxc/image.go:329 msgid "Properties:" msgstr "" -#: lxc/remote.go:52 +#: lxc/remote.go:55 msgid "Public image server" msgstr "" -#: lxc/image.go:298 +#: lxc/image.go:317 #, c-format msgid "Public: %s" msgstr "" @@ -813,7 +818,7 @@ msgid "Publish containers as images.\n" "lxc publish [remote:]container [remote:] [--alias=ALIAS]... [prop-key=prop-value]..." msgstr "" -#: lxc/remote.go:51 +#: lxc/remote.go:53 msgid "Remote admin password" msgstr "" @@ -826,35 +831,39 @@ msgstr "" msgid "Require user confirmation." msgstr "" -#: lxc/init.go:247 +#: lxc/init.go:246 #, c-format msgid "Retrieving image: %s" msgstr "" -#: lxc/image.go:566 +#: lxc/image.go:595 msgid "SIZE" msgstr "" -#: lxc/list.go:339 +#: lxc/list.go:340 msgid "SNAPSHOTS" msgstr "" -#: lxc/list.go:340 +#: lxc/list.go:341 msgid "STATE" msgstr "" -#: lxc/remote.go:316 +#: lxc/remote.go:327 msgid "STATIC" msgstr "" -#: lxc/remote.go:164 +#: lxc/remote.go:174 msgid "Server certificate NACKed by user" msgstr "" -#: lxc/remote.go:226 +#: lxc/remote.go:236 msgid "Server doesn't trust us after adding our cert" msgstr "" +#: lxc/remote.go:54 +msgid "Server protocol (lxd or simplestreams)" +msgstr "" + #: lxc/restore.go:21 msgid "Set the current state of a resource back to a snapshot.\n" "\n" @@ -888,7 +897,7 @@ msgstr "" msgid "Show the container's last 100 log lines?" msgstr "" -#: lxc/image.go:296 +#: lxc/image.go:315 #, c-format msgid "Size: %.2fMB" msgstr "" @@ -897,6 +906,10 @@ msgstr "" msgid "Snapshots:" msgstr "" +#: lxc/image.go:339 +msgid "Source:" +msgstr "" + #: lxc/launch.go:122 #, c-format msgid "Starting %s" @@ -919,7 +932,7 @@ msgstr "" msgid "Store the container state (only for stop)." msgstr "" -#: lxc/list.go:341 +#: lxc/list.go:342 msgid "TYPE" msgstr "" @@ -939,11 +952,11 @@ msgstr "" msgid "Time to wait for the container before killing it." msgstr "" -#: lxc/image.go:299 +#: lxc/image.go:318 msgid "Timestamps:" msgstr "" -#: lxc/image.go:362 +#: lxc/image.go:388 #, c-format msgid "Transfering image: %d%%" msgstr "" @@ -961,15 +974,15 @@ msgstr "" msgid "Type: persistent" msgstr "" -#: lxc/image.go:567 +#: lxc/image.go:596 msgid "UPLOAD DATE" msgstr "" -#: lxc/remote.go:313 +#: lxc/remote.go:324 msgid "URL" msgstr "" -#: lxc/image.go:304 +#: lxc/image.go:323 #, c-format msgid "Uploaded: %s" msgstr "" @@ -999,7 +1012,7 @@ msgstr "" msgid "Whether to show the expanded configuration" msgstr "" -#: lxc/remote.go:289 lxc/remote.go:294 +#: lxc/remote.go:299 lxc/remote.go:304 msgid "YES" msgstr "" @@ -1019,11 +1032,11 @@ msgstr "" msgid "can't copy to the same container name" msgstr "" -#: lxc/remote.go:277 +#: lxc/remote.go:287 msgid "can't remove the default remote" msgstr "" -#: lxc/remote.go:303 +#: lxc/remote.go:313 msgid "default" msgstr "" @@ -1031,7 +1044,15 @@ msgstr "" msgid "didn't get any affected image, container or snapshot from server" msgstr "" -#: lxc/main.go:25 lxc/main.go:157 +#: lxc/image.go:309 +msgid "disabled" +msgstr "" + +#: lxc/image.go:311 +msgid "enabled" +msgstr "" + +#: lxc/main.go:25 lxc/main.go:154 #, c-format msgid "error: %v" msgstr "" @@ -1045,7 +1066,7 @@ msgstr "" msgid "got bad version" msgstr "" -#: lxc/image.go:290 lxc/image.go:544 +#: lxc/image.go:304 lxc/image.go:572 msgid "no" msgstr "" @@ -1053,31 +1074,31 @@ msgstr "" msgid "not all the profiles from the source exist on the target" msgstr "" -#: lxc/remote.go:157 +#: lxc/remote.go:167 msgid "ok (y/n)?" msgstr "" -#: lxc/main.go:264 lxc/main.go:268 +#: lxc/main.go:261 lxc/main.go:265 #, c-format msgid "processing aliases failed %s\n" msgstr "" -#: lxc/remote.go:338 +#: lxc/remote.go:349 #, c-format msgid "remote %s already exists" msgstr "" -#: lxc/remote.go:269 lxc/remote.go:330 lxc/remote.go:365 lxc/remote.go:381 +#: lxc/remote.go:279 lxc/remote.go:341 lxc/remote.go:376 lxc/remote.go:392 #, c-format msgid "remote %s doesn't exist" msgstr "" -#: lxc/remote.go:252 +#: lxc/remote.go:262 #, c-format msgid "remote %s exists as <%s>" msgstr "" -#: lxc/remote.go:273 lxc/remote.go:334 lxc/remote.go:369 +#: lxc/remote.go:283 lxc/remote.go:345 lxc/remote.go:380 #, c-format msgid "remote %s is static and cannot be modified" msgstr "" @@ -1099,11 +1120,11 @@ msgstr "" msgid "unreachable return reached" msgstr "" -#: lxc/main.go:197 +#: lxc/main.go:194 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:306 lxc/image.go:576 msgid "yes" msgstr "" diff --git a/scripts/lxd-images b/scripts/lxd-images index a880070cc381..195f4bf521bd 100755 --- a/scripts/lxd-images +++ b/scripts/lxd-images @@ -562,6 +562,12 @@ if __name__ == "__main__": setup_alias(args.alias, fingerprint) def import_ubuntu(parser, args): + sys.stderr.write( + 'lxd-images is deprecated and will gone by LXD 2.0 final\n') + sys.stderr.write( + 'Please update use the ubuntu: and ubuntu-daily: remotes\n') + sys.stderr.write('\n') + if args.stream == "auto": for stream in ("releases", "daily"): ubuntu = Ubuntu(stream=stream) diff --git a/shared/image.go b/shared/image.go index c2feaaa76327..e2e39d4735e4 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/shared/simplestreams.go b/shared/simplestreams.go index e9843f621f3d..54cf48847d77 100644 --- a/shared/simplestreams.go +++ b/shared/simplestreams.go @@ -148,6 +148,7 @@ func (s *SimpleStreamsManifest) ToLXD() ([]ImageInfo, map[string][][]string) { image.Filename = filename image.Fingerprint = fingerprint image.Properties = map[string]string{ + "aliases": product.Aliases, "os": product.OperatingSystem, "release": product.Release, "version": product.Version, @@ -174,6 +175,7 @@ func (s *SimpleStreamsManifest) ToLXD() ([]ImageInfo, map[string][][]string) { } type SimpleStreamsManifestProduct struct { + Aliases string `json:"aliases"` Architecture string `json:"arch"` OperatingSystem string `json:"os"` Release string `json:"release"` @@ -348,71 +350,102 @@ func (s *SimpleStreams) applyAliases(images []ImageInfo) ([]ImageInfo, map[strin newImages := []ImageInfo{} for _, image := range images { - // Short - if image.Architecture == architectureName { - alias := addAlias(fmt.Sprintf("%s/%s", image.Properties["os"], image.Properties["release"]), image.Fingerprint) + if image.Properties["aliases"] != "" { + aliases := strings.Split(image.Properties["aliases"], ",") + for _, entry := range aliases { + // Short + if image.Architecture == architectureName { + alias := addAlias(fmt.Sprintf("%s", entry), image.Fingerprint) + if alias != nil { + image.Aliases = append(image.Aliases, *alias) + } + + alias = addAlias(fmt.Sprintf("%s/%s", entry, image.Properties["serial"]), image.Fingerprint) + if alias != nil { + image.Aliases = append(image.Aliases, *alias) + } + } + + // Medium + alias := addAlias(fmt.Sprintf("%s/%s", entry, image.Properties["architecture"]), image.Fingerprint) + if alias != nil { + image.Aliases = append(image.Aliases, *alias) + } + + // Long + alias = addAlias(fmt.Sprintf("%s/%s/%s", entry, image.Properties["architecture"], image.Properties["serial"]), image.Fingerprint) + if alias != nil { + image.Aliases = append(image.Aliases, *alias) + } + } + } else { + // FIXME: This is backward compatibility needed until cloud-images.ubuntu.com supports the aliases field + // Short + if image.Architecture == architectureName { + alias := addAlias(fmt.Sprintf("%s/%s", image.Properties["os"], image.Properties["release"]), image.Fingerprint) + if alias != nil { + image.Aliases = append(image.Aliases, *alias) + } + + alias = addAlias(fmt.Sprintf("%s/%s/%s", image.Properties["os"], image.Properties["release"], image.Properties["serial"]), image.Fingerprint) + if alias != nil { + image.Aliases = append(image.Aliases, *alias) + } + + alias = addAlias(fmt.Sprintf("%s/%c", image.Properties["os"], image.Properties["release"][0]), image.Fingerprint) + if alias != nil { + image.Aliases = append(image.Aliases, *alias) + } + + alias = addAlias(fmt.Sprintf("%s/%c/%s", image.Properties["os"], image.Properties["release"][0], image.Properties["serial"]), image.Fingerprint) + if alias != nil { + image.Aliases = append(image.Aliases, *alias) + } + + alias = addAlias(fmt.Sprintf("%s/%s", image.Properties["os"], image.Properties["version"]), image.Fingerprint) + if alias != nil { + image.Aliases = append(image.Aliases, *alias) + } + + alias = addAlias(fmt.Sprintf("%s/%s/%s", image.Properties["os"], image.Properties["version"], image.Properties["serial"]), image.Fingerprint) + if alias != nil { + image.Aliases = append(image.Aliases, *alias) + } + } + + // Medium + alias := addAlias(fmt.Sprintf("%s/%s/%s", image.Properties["os"], image.Properties["release"], image.Properties["architecture"]), image.Fingerprint) if alias != nil { image.Aliases = append(image.Aliases, *alias) } - alias = addAlias(fmt.Sprintf("%s/%s/%s", image.Properties["os"], image.Properties["release"], image.Properties["serial"]), image.Fingerprint) + alias = addAlias(fmt.Sprintf("%s/%c/%s", image.Properties["os"], image.Properties["release"][0], image.Properties["architecture"]), image.Fingerprint) if alias != nil { image.Aliases = append(image.Aliases, *alias) } - alias = addAlias(fmt.Sprintf("%s/%c", image.Properties["os"], image.Properties["release"][0]), image.Fingerprint) + alias = addAlias(fmt.Sprintf("%s/%s/%s", image.Properties["os"], image.Properties["version"], image.Properties["architecture"]), image.Fingerprint) if alias != nil { image.Aliases = append(image.Aliases, *alias) } - alias = addAlias(fmt.Sprintf("%s/%c/%s", image.Properties["os"], image.Properties["release"][0], image.Properties["serial"]), image.Fingerprint) + // Long + alias = addAlias(fmt.Sprintf("%s/%s/%s/%s", image.Properties["os"], image.Properties["release"], image.Properties["architecture"], image.Properties["serial"]), image.Fingerprint) if alias != nil { image.Aliases = append(image.Aliases, *alias) } - alias = addAlias(fmt.Sprintf("%s/%s", image.Properties["os"], image.Properties["version"]), image.Fingerprint) + alias = addAlias(fmt.Sprintf("%s/%c/%s/%s", image.Properties["os"], image.Properties["release"][0], image.Properties["architecture"], image.Properties["serial"]), image.Fingerprint) if alias != nil { image.Aliases = append(image.Aliases, *alias) } - alias = addAlias(fmt.Sprintf("%s/%s/%s", image.Properties["os"], image.Properties["version"], image.Properties["serial"]), image.Fingerprint) + alias = addAlias(fmt.Sprintf("%s/%s/%s/%s", image.Properties["os"], image.Properties["version"], image.Properties["architecture"], image.Properties["serial"]), image.Fingerprint) if alias != nil { image.Aliases = append(image.Aliases, *alias) } } - // Medium - alias := addAlias(fmt.Sprintf("%s/%s/%s", image.Properties["os"], image.Properties["release"], image.Properties["architecture"]), image.Fingerprint) - if alias != nil { - image.Aliases = append(image.Aliases, *alias) - } - - alias = addAlias(fmt.Sprintf("%s/%c/%s", image.Properties["os"], image.Properties["release"][0], image.Properties["architecture"]), image.Fingerprint) - if alias != nil { - image.Aliases = append(image.Aliases, *alias) - } - - alias = addAlias(fmt.Sprintf("%s/%s/%s", image.Properties["os"], image.Properties["version"], image.Properties["architecture"]), image.Fingerprint) - if alias != nil { - image.Aliases = append(image.Aliases, *alias) - } - - // Medium - alias = addAlias(fmt.Sprintf("%s/%s/%s/%s", image.Properties["os"], image.Properties["release"], image.Properties["architecture"], image.Properties["serial"]), image.Fingerprint) - if alias != nil { - image.Aliases = append(image.Aliases, *alias) - } - - alias = addAlias(fmt.Sprintf("%s/%c/%s/%s", image.Properties["os"], image.Properties["release"][0], image.Properties["architecture"], image.Properties["serial"]), image.Fingerprint) - if alias != nil { - image.Aliases = append(image.Aliases, *alias) - } - - alias = addAlias(fmt.Sprintf("%s/%s/%s/%s", image.Properties["os"], image.Properties["version"], image.Properties["architecture"], image.Properties["serial"]), image.Fingerprint) - if alias != nil { - image.Aliases = append(image.Aliases, *alias) - } - newImages = append(newImages, image) } diff --git a/specs/configuration.md b/specs/configuration.md index fe7e6f781e73..efa0430686ac 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 1c3a70380992..fd7de4f643e8 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 034f9df2f40f..3d9f3144205e 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 bb37f795f9ab..d89d7575f8a1 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; } }