[lxc-devel] [lxd/master] New client library
stgraber on Github
lxc-bot at linuxcontainers.org
Fri Mar 31 23:36:27 UTC 2017
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/20170331/d3c1ec54/attachment.bin>
-------------- next part --------------
From a39605e379d5bdd569171dac2a37afe7f2f71cdf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Thu, 15 Dec 2016 16:46:14 -0500
Subject: [PATCH 01/15] client: New client library
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Closes #1926
Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
client/connection.go | 174 +++++++++++++
client/doc.go | 154 +++++++++++
client/events.go | 96 +++++++
client/interfaces.go | 244 ++++++++++++++++++
client/lxd.go | 201 +++++++++++++++
client/lxd_certificates.go | 93 +++++++
client/lxd_containers.go | 562 +++++++++++++++++++++++++++++++++++++++++
client/lxd_events.go | 105 ++++++++
client/lxd_images.go | 389 ++++++++++++++++++++++++++++
client/lxd_networks.go | 102 ++++++++
client/lxd_operations.go | 43 ++++
client/lxd_profiles.go | 100 ++++++++
client/lxd_server.go | 50 ++++
client/lxd_storage_pools.go | 97 +++++++
client/lxd_storage_volumes.go | 93 +++++++
client/operations.go | 221 ++++++++++++++++
client/simplestreams.go | 17 ++
client/simplestreams_images.go | 177 +++++++++++++
client/util.go | 145 +++++++++++
19 files changed, 3063 insertions(+)
create mode 100644 client/connection.go
create mode 100644 client/doc.go
create mode 100644 client/events.go
create mode 100644 client/interfaces.go
create mode 100644 client/lxd.go
create mode 100644 client/lxd_certificates.go
create mode 100644 client/lxd_containers.go
create mode 100644 client/lxd_events.go
create mode 100644 client/lxd_images.go
create mode 100644 client/lxd_networks.go
create mode 100644 client/lxd_operations.go
create mode 100644 client/lxd_profiles.go
create mode 100644 client/lxd_server.go
create mode 100644 client/lxd_storage_pools.go
create mode 100644 client/lxd_storage_volumes.go
create mode 100644 client/operations.go
create mode 100644 client/simplestreams.go
create mode 100644 client/simplestreams_images.go
create mode 100644 client/util.go
diff --git a/client/connection.go b/client/connection.go
new file mode 100644
index 0000000..0af923f
--- /dev/null
+++ b/client/connection.go
@@ -0,0 +1,174 @@
+package lxd
+
+import (
+ "net/http"
+ "net/url"
+ "os"
+ "path/filepath"
+
+ "github.com/lxc/lxd/shared/simplestreams"
+)
+
+// ConnectionArgs represents a set of common connection properties
+type ConnectionArgs struct {
+ // TLS certificate of the remote server. If not specified, the system CA is used.
+ TLSServerCert string
+
+ // TLS certificate to use for client authentication.
+ TLSClientCert string
+
+ // TLS key to use for client authentication.
+ TLSClientKey string
+
+ // TLS CA to validate against when in PKI mode.
+ TLSCA string
+
+ // User agent string
+ UserAgent string
+
+ // Custom proxy
+ Proxy func(*http.Request) (*url.URL, error)
+}
+
+// ConnectLXD lets you connect to a remote LXD daemon over HTTPs.
+//
+// A client certificate (TLSClientCert) and key (TLSClientKey) must be provided.
+//
+// If connecting to a LXD daemon running in PKI mode, the PKI CA (TLSCA) must also be provided.
+//
+// Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert).
+func ConnectLXD(url string, args *ConnectionArgs) (ContainerServer, error) {
+ // Use empty args if not specified
+ if args == nil {
+ args = &ConnectionArgs{}
+ }
+
+ // Initialize the client struct
+ server := ProtocolLXD{
+ httpHost: url,
+ httpUserAgent: args.UserAgent,
+ httpCertificate: args.TLSClientCert,
+ }
+
+ // Setup the HTTP client
+ httpClient, err := tlsHTTPClient(args.TLSClientCert, args.TLSClientKey, args.TLSCA, args.TLSServerCert, args.Proxy)
+ if err != nil {
+ return nil, err
+ }
+ server.http = httpClient
+
+ // Test the connection and seed the server information
+ _, _, err = server.GetServer()
+ if err != nil {
+ return nil, err
+ }
+
+ return &server, nil
+}
+
+// ConnectLXDUnix lets you connect to a remote LXD daemon over a local unix socket.
+//
+// If the path argument is empty, then $LXD_DIR/unix.socket will be used.
+// If that one isn't set either, then the path will default to /var/lib/lxd/unix.socket.
+func ConnectLXDUnix(path string, args *ConnectionArgs) (ContainerServer, error) {
+ // Use empty args if not specified
+ if args == nil {
+ args = &ConnectionArgs{}
+ }
+
+ // Initialize the client struct
+ server := ProtocolLXD{
+ httpHost: "http://unix.socket",
+ httpUserAgent: args.UserAgent,
+ }
+
+ // Determine the socket path
+ if path == "" {
+ lxdDir := os.Getenv("LXD_DIR")
+ if lxdDir == "" {
+ lxdDir = "/var/lib/lxd"
+ }
+
+ path = filepath.Join(lxdDir, "unix.socket")
+ }
+
+ // Setup the HTTP client
+ httpClient, err := unixHTTPClient(path)
+ if err != nil {
+ return nil, err
+ }
+ server.http = httpClient
+
+ // Test the connection and seed the server information
+ serverStatus, _, err := server.GetServer()
+ if err != nil {
+ return nil, err
+ }
+
+ // Record the server certificate
+ server.httpCertificate = serverStatus.Environment.Certificate
+
+ return &server, nil
+}
+
+// ConnectPublicLXD lets you connect to a remote public LXD daemon over HTTPs.
+//
+// Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert).
+func ConnectPublicLXD(url string, args *ConnectionArgs) (ImageServer, error) {
+ // Use empty args if not specified
+ if args == nil {
+ args = &ConnectionArgs{}
+ }
+
+ // Initialize the client struct
+ server := ProtocolLXD{
+ httpHost: url,
+ httpUserAgent: args.UserAgent,
+ httpCertificate: args.TLSClientCert,
+ }
+
+ // Setup the HTTP client
+ httpClient, err := tlsHTTPClient(args.TLSClientCert, args.TLSClientKey, args.TLSCA, args.TLSServerCert, args.Proxy)
+ if err != nil {
+ return nil, err
+ }
+ server.http = httpClient
+
+ // Test the connection and seed the server information
+ _, _, err = server.GetServer()
+ if err != nil {
+ return nil, err
+ }
+
+ return &server, nil
+}
+
+// ConnectSimpleStreams lets you connect to a remote SimpleStreams image server over HTTPs.
+//
+// Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert).
+func ConnectSimpleStreams(url string, args *ConnectionArgs) (ImageServer, error) {
+ // Use empty args if not specified
+ if args == nil {
+ args = &ConnectionArgs{}
+ }
+
+ // Initialize the client struct
+ server := ProtocolSimpleStreams{
+ httpHost: url,
+ httpUserAgent: args.UserAgent,
+ httpCertificate: args.TLSClientCert,
+ }
+
+ // Setup the HTTP client
+ httpClient, err := tlsHTTPClient(args.TLSClientCert, args.TLSClientKey, args.TLSCA, args.TLSServerCert, args.Proxy)
+ if err != nil {
+ return nil, err
+ }
+ server.http = httpClient
+
+ // Get simplestreams client
+ ssClient := simplestreams.NewClient(url, *httpClient, args.UserAgent)
+ server.ssClient = ssClient
+
+ return &server, nil
+}
diff --git a/client/doc.go b/client/doc.go
new file mode 100644
index 0000000..f54f5b9
--- /dev/null
+++ b/client/doc.go
@@ -0,0 +1,154 @@
+// Package lxd implements a client for the LXD API
+//
+// Warning
+//
+// This API isn't considered STABLE yet!
+//
+// This client library is planned to become the new supported Go library
+// for LXD which will come with guaranteed API stability. New functions and
+// struct arguments may be added over time but no existing signature or
+// type will be changed and structs will only gain new members.
+//
+// Overview
+//
+// This package lets you connect to LXD daemons or SimpleStream image
+// servers over a Unix socket or HTTPs. You can then interact with those
+// remote servers, creating containers, images, moving them around, ...
+//
+// Example - container creation
+//
+// This creates a container on a local LXD daemon and then starts it.
+//
+// // Connect to LXD over the Unix socket
+// c, err := lxd.ConnectLXDUnix("", nil)
+// if err != nil {
+// return err
+// }
+//
+// // Container creation request
+// req := api.ContainersPost{
+// Name: "my-container",
+// Source: api.ContainerSource{
+// Type: "image",
+// Alias: "my-image",
+// },
+// }
+//
+// // Get LXD to create the container (background operation)
+// op, err := c.CreateContainer(req)
+// if err != nil {
+// return err
+// }
+//
+// // Wait for the operation to complete
+// err = op.Wait()
+// if err != nil {
+// return err
+// }
+//
+// // Get LXD to start the container (background operation)
+// reqState := api.ContainerStatePut{
+// Action: "start",
+// Timeout: -1,
+// }
+//
+// op, err = c.UpdateContainerState(name, reqState, "")
+// if err != nil {
+// return err
+// }
+//
+// // Wait for the operation to complete
+// err = op.Wait()
+// if err != nil {
+// return err
+// }
+//
+// Example - command execution
+//
+// This executes an interactive bash terminal
+//
+// // Connect to LXD over the Unix socket
+// c, err := lxd.ConnectLXDUnix("", nil)
+// if err != nil {
+// return err
+// }
+//
+// // Setup the exec request
+// req := api.ContainerExecPost{
+// Command: []string{"bash"},
+// WaitForWS: true,
+// Interactive: true,
+// Width: 80,
+// Height: 15,
+// }
+//
+// // Setup the exec arguments (fds)
+// args := lxd.ContainerExecArgs{
+// Stdin: os.Stdin,
+// Stdout: os.Stdout,
+// Stderr: os.Stderr,
+// }
+//
+// // Setup the terminal (set to raw mode)
+// if req.Interactive {
+// cfd := int(syscall.Stdin)
+// oldttystate, err := termios.MakeRaw(cfd)
+// if err != nil {
+// return err
+// }
+//
+// defer termios.Restore(cfd, oldttystate)
+// }
+//
+// // Get the current state
+// op, err := c.ExecContainer("c1", req, &args)
+// if err != nil {
+// return err
+// }
+//
+// // Wait for it to complete
+// err = op.Wait()
+// if err != nil {
+// return err
+// }
+//
+// Example - image copy
+//
+// This copies an image from a simplestreams server to a local LXD daemon
+//
+// // Connect to LXD over the Unix socket
+// c, err := lxd.ConnectLXDUnix("", nil)
+// if err != nil {
+// return err
+// }
+//
+// // Connect to the remote SimpleStreams server
+// d, err = lxd.ConnectSimpleStreams("https://images.linuxcontainers.org", nil)
+// if err != nil {
+// return err
+// }
+//
+// // Resolve the alias
+// alias, _, err := d.GetImageAlias("centos/7")
+// if err != nil {
+// return err
+// }
+//
+// // Get the image information
+// image, _, err := d.GetImage(alias.Target)
+// if err != nil {
+// return err
+// }
+//
+// // Ask LXD to copy the image from the remote server
+// op, err := d.CopyImage(*image, c, nil)
+// if err != nil {
+// return err
+// }
+//
+// // And wait for it to finish
+// err = op.Wait()
+// if err != nil {
+// return err
+// }
+package lxd
diff --git a/client/events.go b/client/events.go
new file mode 100644
index 0000000..f97604b
--- /dev/null
+++ b/client/events.go
@@ -0,0 +1,96 @@
+package lxd
+
+import (
+ "fmt"
+ "sync"
+)
+
+// The EventListener struct is used to interact with a LXD event stream
+type EventListener struct {
+ r *ProtocolLXD
+ chActive chan bool
+ disconnected bool
+ err error
+
+ targets []*EventTarget
+ targetsLock sync.Mutex
+}
+
+// The EventTarget struct is returned to the caller of AddHandler and used in RemoveHandler
+type EventTarget struct {
+ function func(interface{})
+ types []string
+}
+
+// AddHandler adds a function to be called whenever an event is received
+func (e *EventListener) AddHandler(types []string, function func(interface{})) (*EventTarget, error) {
+ if function == nil {
+ return nil, fmt.Errorf("A valid function must be provided")
+ }
+
+ // Handle locking
+ e.targetsLock.Lock()
+ defer e.targetsLock.Unlock()
+
+ // Create a new target
+ target := EventTarget{
+ function: function,
+ types: types,
+ }
+
+ // And add it to the targets
+ e.targets = append(e.targets, &target)
+
+ return &target, nil
+}
+
+// RemoveHandler removes a function to be called whenever an event is received
+func (e *EventListener) RemoveHandler(target *EventTarget) error {
+ if target == nil {
+ return fmt.Errorf("A valid event target must be provided")
+ }
+
+ // Handle locking
+ e.targetsLock.Lock()
+ defer e.targetsLock.Unlock()
+
+ // Locate and remove the function from the list
+ for i, entry := range e.targets {
+ if entry == target {
+ e.targets = append(e.targets[:i], e.targets[i+1:]...)
+ return nil
+ }
+ }
+
+ return fmt.Errorf("Couldn't find this function and event types combination")
+}
+
+// Disconnect must be used once done listening for events
+func (e *EventListener) Disconnect() {
+ if e.disconnected {
+ return
+ }
+
+ // Handle locking
+ e.r.eventListenersLock.Lock()
+ defer e.r.eventListenersLock.Unlock()
+
+ // Locate and remove it from the global list
+ for i, listener := range e.r.eventListeners {
+ if listener == e {
+ e.r.eventListeners = append(e.r.eventListeners[:i], e.r.eventListeners[i+1:]...)
+ break
+ }
+ }
+
+ // Turn off the handler
+ e.err = nil
+ e.disconnected = true
+ close(e.chActive)
+}
+
+// Wait hangs until the server disconnects the connection or Disconnect() is called
+func (e *EventListener) Wait() error {
+ <-e.chActive
+ return e.err
+}
diff --git a/client/interfaces.go b/client/interfaces.go
new file mode 100644
index 0000000..6f3c25b
--- /dev/null
+++ b/client/interfaces.go
@@ -0,0 +1,244 @@
+package lxd
+
+import (
+ "io"
+
+ "github.com/gorilla/websocket"
+
+ "github.com/lxc/lxd/shared/api"
+)
+
+// The ImageServer type represents a read-only image server.
+type ImageServer interface {
+ // Image handling functions
+ GetImages() (images []api.Image, err error)
+ GetImageFingerprints() (fingerprints []string, err error)
+
+ GetImage(fingerprint string) (image *api.Image, ETag string, err error)
+ GetImageFile(fingerprint string, req ImageFileRequest) (resp *ImageFileResponse, err error)
+
+ GetPrivateImage(fingerprint string, secret string) (image *api.Image, ETag string, err error)
+ GetPrivateImageFile(fingerprint string, secret string, req ImageFileRequest) (resp *ImageFileResponse, err error)
+
+ GetImageAliases() (aliases []api.ImageAliasesEntry, err error)
+ GetImageAliasNames() (names []string, err error)
+
+ GetImageAlias(name string) (alias *api.ImageAliasesEntry, ETag string, err error)
+
+ CopyImage(image api.Image, target ContainerServer, args *ImageCopyArgs) (op *Operation, err error)
+}
+
+// The ContainerServer type represents a full featured LXD server.
+type ContainerServer interface {
+ // Server functions
+ GetServer() (server *api.Server, ETag string, err error)
+ UpdateServer(server api.ServerPut, ETag string) (err error)
+ HasExtension(extension string) bool
+
+ // Certificate functions
+ GetCertificateFingerprints() (fingerprints []string, err error)
+ GetCertificates() (certificates []api.Certificate, err error)
+ GetCertificate(fingerprint string) (certificate *api.Certificate, ETag string, err error)
+ CreateCertificate(certificate api.CertificatesPost) (err error)
+ UpdateCertificate(fingerprint string, certificate api.CertificatePut, ETag string) (err error)
+ DeleteCertificate(fingerprint string) (err error)
+
+ // Container functions
+ GetContainerNames() (names []string, err error)
+ GetContainers() (containers []api.Container, err error)
+ GetContainer(name string) (container *api.Container, ETag string, err error)
+ CreateContainer(container api.ContainersPost) (op *Operation, err error)
+ UpdateContainer(name string, container api.ContainerPut, ETag string) (op *Operation, err error)
+ RenameContainer(name string, container api.ContainerPost) (op *Operation, err error)
+ MigrateContainer(name string, container api.ContainerPost) (op *Operation, err error)
+ DeleteContainer(name string) (op *Operation, err error)
+
+ ExecContainer(containerName string, exec api.ContainerExecPost, args *ContainerExecArgs) (*Operation, error)
+
+ GetContainerFile(containerName string, path string) (content io.ReadCloser, resp *ContainerFileResponse, err error)
+ CreateContainerFile(containerName string, path string, args ContainerFileArgs) (err error)
+ DeleteContainerFile(containerName string, path string) (err error)
+
+ GetContainerSnapshotNames(containerName string) (names []string, err error)
+ GetContainerSnapshots(containerName string) (snapshots []api.ContainerSnapshot, err error)
+ GetContainerSnapshot(containerName string, name string) (snapshot *api.ContainerSnapshot, ETag string, err error)
+ CreateContainerSnapshot(containerName string, snapshot api.ContainerSnapshotsPost) (op *Operation, err error)
+ RenameContainerSnapshot(containerName string, name string, container api.ContainerSnapshotPost) (op *Operation, err error)
+ MigrateContainerSnapshot(containerName string, name string, container api.ContainerSnapshotPost) (op *Operation, err error)
+ DeleteContainerSnapshot(containerName string, name string) (op *Operation, err error)
+
+ GetContainerState(name string) (state *api.ContainerState, ETag string, err error)
+ UpdateContainerState(name string, state api.ContainerStatePut, ETag string) (op *Operation, err error)
+
+ GetContainerLogfiles(name string) (logfiles []string, err error)
+ GetContainerLogfile(name string, filename string) (content io.ReadCloser, err error)
+ DeleteContainerLogfile(name string, filename string) (err error)
+
+ // Event handling functions
+ GetEvents() (listener *EventListener, err error)
+
+ // Image functions
+ ImageServer
+ CreateImage(image api.ImagesPost) (op *Operation, err error)
+ UpdateImage(fingerprint string, image api.ImagePut, ETag string) (err error)
+ DeleteImage(fingerprint string) (op *Operation, err error)
+ CreateImageSecret(fingerprint string) (op *Operation, err error)
+ CreateImageAlias(alias api.ImageAliasesPost) (err error)
+ UpdateImageAlias(name string, alias api.ImageAliasesEntryPut, ETag string) (err error)
+ RenameImageAlias(name string, alias api.ImageAliasesEntryPost) (err error)
+ DeleteImageAlias(name string) (err error)
+
+ // Network functions ("network" API extension)
+ GetNetworkNames() (names []string, err error)
+ GetNetworks() (networks []api.Network, err error)
+ GetNetwork(name string) (network *api.Network, ETag string, err error)
+ CreateNetwork(network api.NetworksPost) (err error)
+ UpdateNetwork(name string, network api.NetworkPut, ETag string) (err error)
+ RenameNetwork(name string, network api.NetworkPost) (err error)
+ DeleteNetwork(name string) (err error)
+
+ // Operation functions
+ GetOperation(uuid string) (op *api.Operation, ETag string, err error)
+ DeleteOperation(uuid string) (err error)
+ GetOperationWebsocket(uuid string, secret string) (conn *websocket.Conn, err error)
+
+ // Profile functions
+ GetProfileNames() (names []string, err error)
+ GetProfiles() (profiles []api.Profile, err error)
+ GetProfile(name string) (profile *api.Profile, ETag string, err error)
+ CreateProfile(profile api.ProfilesPost) (err error)
+ UpdateProfile(name string, profile api.ProfilePut, ETag string) (err error)
+ RenameProfile(name string, profile api.ProfilePost) (err error)
+ DeleteProfile(name string) (err error)
+
+ // Storage pool functions ("storage" API extension)
+ GetStoragePoolNames() (names []string, err error)
+ GetStoragePools() (pools []api.StoragePool, err error)
+ GetStoragePool(name string) (pool *api.StoragePool, ETag string, err error)
+ CreateStoragePool(pool api.StoragePoolsPost) (err error)
+ UpdateStoragePool(name string, pool api.StoragePoolPut, ETag string) (err error)
+ DeleteStoragePool(name string) (err error)
+
+ // Storage volume functions ("storage" API extension)
+ GetStoragePoolVolumeNames(pool string) (names []string, err error)
+ GetStoragePoolVolumes(pool string) (volumes []api.StorageVolume, err error)
+ GetStoragePoolVolume(pool string, name string) (volume *api.StorageVolume, ETag string, err error)
+ CreateStoragePoolVolume(pool string, volume api.StorageVolumesPost) (err error)
+ UpdateStoragePoolVolume(pool string, name string, volume api.StorageVolumePut, ETag string) (err error)
+ DeleteStoragePoolVolume(pool string, name string) (err error)
+
+ // Internal functions (for internal use)
+ RawQuery(method string, path string, data interface{}, queryETag string) (resp *api.Response, ETag string, err error)
+ RawWebsocket(path string) (conn *websocket.Conn, err error)
+}
+
+// The ProgressData struct represents new progress information on an operation
+type ProgressData struct {
+ // Preferred string repreentation of progress (always set)
+ Text string
+
+ // Progress in percent
+ Percentage int
+
+ // Number of bytes transferred (for files)
+ TransferredBytes int64
+
+ // Total number of bytes (for files)
+ TotalBytes int64
+}
+
+// The ImageFileRequest struct is used for an image download request
+type ImageFileRequest struct {
+ // Writer for the metadata file
+ MetaFile io.WriteSeeker
+
+ // Writer for the rootfs file
+ RootfsFile io.WriteSeeker
+
+ // Progress handler (called whenever some progress is made)
+ ProgressHandler func(progress ProgressData)
+}
+
+// The ImageFileResponse struct is used as the response for image downloads
+type ImageFileResponse struct {
+ // Filename for the metadata file
+ MetaName string
+
+ // Size of the metadata file
+ MetaSize int64
+
+ // Filename for the rootfs file
+ RootfsName string
+
+ // Size of the rootfs file
+ RootfsSize int64
+}
+
+// The ImageCopyArgs struct is used to pass additional options during image copy
+type ImageCopyArgs struct {
+ // Aliases to add to the copied image.
+ Aliases []api.ImageAlias
+
+ // Whether to have LXD keep this image up to date
+ AutoUpdate bool
+
+ // Whether to copy the source image aliases to the target
+ CopyAliases bool
+
+ // Whether this image is to be made available to unauthenticated users
+ Public bool
+}
+
+// The ContainerExecArgs struct is used to pass additional options during container exec
+type ContainerExecArgs struct {
+ // Standard input
+ Stdin io.ReadCloser
+
+ // Standard output
+ Stdout io.WriteCloser
+
+ // Standard error
+ Stderr io.WriteCloser
+
+ // Control message handler (window resize, signals, ...)
+ Control func(conn *websocket.Conn)
+}
+
+// The ContainerFileArgs struct is used to pass the various options for a container file upload
+type ContainerFileArgs struct {
+ // File content
+ Content io.ReadSeeker
+
+ // User id that owns the file
+ UID int64
+
+ // Group id that owns the file
+ GID int64
+
+ // File permissions
+ Mode int
+
+ // File type (file or directory)
+ Type string
+
+ // File write mode (overwrite or append)
+ WriteMode string
+}
+
+// The ContainerFileResponse struct is used as part of the response for a container file download
+type ContainerFileResponse struct {
+ // User id that owns the file
+ UID int64
+
+ // Group id that owns the file
+ GID int64
+
+ // File permissions
+ Mode int
+
+ // File type (file or directory)
+ Type string
+
+ // If a directory, the list of files inside it
+ Entries []string
+}
diff --git a/client/lxd.go b/client/lxd.go
new file mode 100644
index 0000000..2d7707f
--- /dev/null
+++ b/client/lxd.go
@@ -0,0 +1,201 @@
+package lxd
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+ "sync"
+
+ "github.com/gorilla/websocket"
+
+ "github.com/lxc/lxd/shared/api"
+)
+
+// ProtocolLXD represents a LXD API server
+type ProtocolLXD struct {
+ server *api.Server
+
+ eventListeners []*EventListener
+ eventListenersLock sync.Mutex
+
+ http *http.Client
+ httpHost string
+ httpUserAgent string
+ httpCertificate string
+}
+
+// RawQuery allows directly querying the LXD API
+//
+// This should only be used by internal LXD tools.
+func (r *ProtocolLXD) RawQuery(method string, path string, data interface{}, ETag string) (*api.Response, string, error) {
+ // Generate the URL
+ url := fmt.Sprintf("%s%s", r.httpHost, path)
+
+ return r.rawQuery(method, url, data, ETag)
+}
+
+// RawWebsocket allows directly connection to LXD API websockets
+//
+// This should only be used by internal LXD tools.
+func (r *ProtocolLXD) RawWebsocket(path string) (*websocket.Conn, error) {
+ // Generate the URL
+ url := fmt.Sprintf("%s%s", r.httpHost, path)
+
+ return r.rawWebsocket(url)
+}
+
+// Internal functions
+func (r *ProtocolLXD) rawQuery(method string, url string, data interface{}, ETag string) (*api.Response, string, error) {
+ var req *http.Request
+ var err error
+
+ // Get a new HTTP request setup
+ if data != nil {
+ // Encode the provided data
+ buf := bytes.Buffer{}
+ err := json.NewEncoder(&buf).Encode(data)
+ if err != nil {
+ return nil, "", err
+ }
+
+ // Some data to be sent along with the request
+ req, err = http.NewRequest(method, url, &buf)
+ if err != nil {
+ return nil, "", err
+ }
+
+ // Set the encoding accordingly
+ req.Header.Set("Content-Type", "application/json")
+ } else {
+ // No data to be sent along with the request
+ req, err = http.NewRequest(method, url, nil)
+ if err != nil {
+ return nil, "", err
+ }
+ }
+
+ // Set the user agent
+ if r.httpUserAgent != "" {
+ req.Header.Set("User-Agent", r.httpUserAgent)
+ }
+
+ // Set the ETag
+ if ETag != "" {
+ req.Header.Set("If-Match", ETag)
+ }
+
+ // Send the request
+ resp, err := r.http.Do(req)
+ if err != nil {
+ return nil, "", err
+ }
+ defer resp.Body.Close()
+
+ // Get the ETag
+ etag := resp.Header.Get("ETag")
+
+ // Decode the response
+ decoder := json.NewDecoder(resp.Body)
+ response := api.Response{}
+
+ err = decoder.Decode(&response)
+ if err != nil {
+ // Check the return value for a cleaner error
+ if resp.StatusCode != http.StatusOK {
+ return nil, "", fmt.Errorf("Failed to fetch %s: %s", url, resp.Status)
+ }
+
+ return nil, "", err
+ }
+
+ // Handle errors
+ if response.Type == api.ErrorResponse {
+ return nil, "", fmt.Errorf(response.Error)
+ }
+
+ return &response, etag, nil
+}
+
+func (r *ProtocolLXD) query(method string, path string, data interface{}, ETag string) (*api.Response, string, error) {
+ // Generate the URL
+ url := fmt.Sprintf("%s/1.0%s", r.httpHost, path)
+
+ return r.rawQuery(method, url, data, ETag)
+}
+
+func (r *ProtocolLXD) queryStruct(method string, path string, data interface{}, ETag string, target interface{}) (string, error) {
+ resp, etag, err := r.query(method, path, data, ETag)
+ if err != nil {
+ return "", err
+ }
+
+ err = resp.MetadataAsStruct(&target)
+ if err != nil {
+ return "", err
+ }
+
+ return etag, nil
+}
+
+func (r *ProtocolLXD) queryOperation(method string, path string, data interface{}, ETag string) (*Operation, string, error) {
+ // Send the query
+ resp, etag, err := r.query(method, path, data, ETag)
+ if err != nil {
+ return nil, "", err
+ }
+
+ // Get to the operation
+ respOperation, err := resp.MetadataAsOperation()
+ if err != nil {
+ return nil, "", err
+ }
+
+ // Setup an Operation wrapper
+ op := Operation{
+ Operation: *respOperation,
+ r: r,
+ chActive: make(chan bool),
+ }
+
+ return &op, etag, nil
+}
+
+func (r *ProtocolLXD) rawWebsocket(url string) (*websocket.Conn, error) {
+ // Grab the http transport handler
+ httpTransport := r.http.Transport.(*http.Transport)
+
+ // Setup a new websocket dialer based on it
+ dialer := websocket.Dialer{
+ NetDial: httpTransport.Dial,
+ TLSClientConfig: httpTransport.TLSClientConfig,
+ Proxy: httpTransport.Proxy,
+ }
+
+ // Set the user agent
+ headers := http.Header{}
+ if r.httpUserAgent != "" {
+ headers.Set("User-Agent", r.httpUserAgent)
+ }
+
+ // Establish the connection
+ conn, _, err := dialer.Dial(url, headers)
+ if err != nil {
+ return nil, err
+ }
+
+ return conn, err
+}
+
+func (r *ProtocolLXD) websocket(path string) (*websocket.Conn, error) {
+ // Generate the URL
+ var url string
+ if strings.HasPrefix(r.httpHost, "https://") {
+ url = fmt.Sprintf("wss://%s/1.0%s", strings.TrimPrefix(r.httpHost, "https://"), path)
+ } else {
+ url = fmt.Sprintf("ws://%s/1.0%s", strings.TrimPrefix(r.httpHost, "http://"), path)
+ }
+
+ return r.rawWebsocket(url)
+}
diff --git a/client/lxd_certificates.go b/client/lxd_certificates.go
new file mode 100644
index 0000000..bf53495
--- /dev/null
+++ b/client/lxd_certificates.go
@@ -0,0 +1,93 @@
+package lxd
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/lxc/lxd/shared/api"
+)
+
+// Certificate handling functions
+
+// GetCertificateFingerprints returns a list of certificate fingerprints
+func (r *ProtocolLXD) GetCertificateFingerprints() ([]string, error) {
+ certificates := []string{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", "/certificates", nil, "", &certificates)
+ if err != nil {
+ return nil, err
+ }
+
+ // Parse it
+ fingerprints := []string{}
+ for _, fingerprint := range fingerprints {
+ fields := strings.Split(fingerprint, "/certificates/")
+ fingerprints = append(fingerprints, fields[len(fields)-1])
+ }
+
+ return fingerprints, nil
+}
+
+// GetCertificates returns a list of certificates
+func (r *ProtocolLXD) GetCertificates() ([]api.Certificate, error) {
+ certificates := []api.Certificate{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", "/certificates?recursion=1", nil, "", &certificates)
+ if err != nil {
+ return nil, err
+ }
+
+ return certificates, nil
+}
+
+// GetCertificate returns the certificate entry for the provided fingerprint
+func (r *ProtocolLXD) GetCertificate(fingerprint string) (*api.Certificate, string, error) {
+ certificate := api.Certificate{}
+
+ // Fetch the raw value
+ etag, err := r.queryStruct("GET", fmt.Sprintf("/certificates/%s", fingerprint), nil, "", &certificate)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return &certificate, etag, nil
+}
+
+// CreateCertificate adds a new certificate to the LXD trust store
+func (r *ProtocolLXD) CreateCertificate(certificate api.CertificatesPost) error {
+ // Send the request
+ _, _, err := r.query("POST", "/certificates", certificate, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// UpdateCertificate updates the certificate definition
+func (r *ProtocolLXD) UpdateCertificate(fingerprint string, certificate api.CertificatePut, ETag string) error {
+ if !r.HasExtension("certificate_update") {
+ return fmt.Errorf("The server is missing the required \"certificate_update\" API extension")
+ }
+
+ // Send the request
+ _, _, err := r.query("PUT", fmt.Sprintf("/certificates/%s", fingerprint), certificate, ETag)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// DeleteCertificate removes a certificate from the LXD trust store
+func (r *ProtocolLXD) DeleteCertificate(fingerprint string) error {
+ // Send the request
+ _, _, err := r.query("DELETE", fmt.Sprintf("/certificates/%s", fingerprint), nil, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/client/lxd_containers.go b/client/lxd_containers.go
new file mode 100644
index 0000000..eee315e
--- /dev/null
+++ b/client/lxd_containers.go
@@ -0,0 +1,562 @@
+package lxd
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+
+ "github.com/gorilla/websocket"
+
+ "github.com/lxc/lxd/shared"
+ "github.com/lxc/lxd/shared/api"
+)
+
+// Container handling functions
+
+// GetContainerNames returns a list of container names
+func (r *ProtocolLXD) GetContainerNames() ([]string, error) {
+ urls := []string{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", "/containers", nil, "", &urls)
+ if err != nil {
+ return nil, err
+ }
+
+ // Parse it
+ names := []string{}
+ for _, url := range urls {
+ fields := strings.Split(url, "/containers/")
+ names = append(names, fields[len(fields)-1])
+ }
+
+ return names, nil
+}
+
+// GetContainers returns a list of containers
+func (r *ProtocolLXD) GetContainers() ([]api.Container, error) {
+ containers := []api.Container{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", "/containers?recursion=1", nil, "", &containers)
+ if err != nil {
+ return nil, err
+ }
+
+ return containers, nil
+}
+
+// GetContainer returns the container entry for the provided name
+func (r *ProtocolLXD) GetContainer(name string) (*api.Container, string, error) {
+ container := api.Container{}
+
+ // Fetch the raw value
+ etag, err := r.queryStruct("GET", fmt.Sprintf("/containers/%s", name), nil, "", &container)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return &container, etag, nil
+}
+
+// CreateContainer requests that LXD creates a new container
+func (r *ProtocolLXD) CreateContainer(container api.ContainersPost) (*Operation, error) {
+ if container.Source.ContainerOnly {
+ if !r.HasExtension("container_only_migration") {
+ return nil, fmt.Errorf("The server is missing the required \"container_only_migration\" API extension")
+ }
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", "/containers", container, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// UpdateContainer updates the container definition
+func (r *ProtocolLXD) UpdateContainer(name string, container api.ContainerPut, ETag string) (*Operation, error) {
+ // Send the request
+ op, _, err := r.queryOperation("PUT", fmt.Sprintf("/containers/%s", name), container, ETag)
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// RenameContainer requests that LXD renames the container
+func (r *ProtocolLXD) RenameContainer(name string, container api.ContainerPost) (*Operation, error) {
+ // Sanity check
+ if container.Migration {
+ return nil, fmt.Errorf("Can't ask for a migration through RenameContainer")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("/containers/%s", name), container, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// MigrateContainer requests that LXD prepares for a container migration
+func (r *ProtocolLXD) MigrateContainer(name string, container api.ContainerPost) (*Operation, error) {
+ if container.ContainerOnly {
+ if !r.HasExtension("container_only_migration") {
+ return nil, fmt.Errorf("The server is missing the required \"container_only_migration\" API extension")
+ }
+ }
+
+ // Sanity check
+ if !container.Migration {
+ return nil, fmt.Errorf("Can't ask for a rename through MigrateContainer")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("/containers/%s", name), container, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// DeleteContainer requests that LXD deletes the container
+func (r *ProtocolLXD) DeleteContainer(name string) (*Operation, error) {
+ // Send the request
+ op, _, err := r.queryOperation("DELETE", fmt.Sprintf("/containers/%s", name), nil, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// ExecContainer requests that LXD spawns a command inside the container
+func (r *ProtocolLXD) ExecContainer(containerName string, exec api.ContainerExecPost, args *ContainerExecArgs) (*Operation, error) {
+ if exec.RecordOutput {
+ if !r.HasExtension("container_exec_recording") {
+ return nil, fmt.Errorf("The server is missing the required \"container_exec_recording\" API extension")
+ }
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("/containers/%s/exec", containerName), exec, "")
+ if err != nil {
+ return nil, err
+ }
+
+ // Process additional arguments
+ if args != nil {
+ // Parse the fds
+ fds := map[string]string{}
+
+ value, ok := op.Metadata["fds"]
+ if ok {
+ values := value.(map[string]interface{})
+ for k, v := range values {
+ fds[k] = v.(string)
+ }
+ }
+
+ // Call the control handler with a connection to the control socket
+ if args.Control != nil && fds["control"] != "" {
+ conn, err := r.GetOperationWebsocket(op.ID, fds["control"])
+ if err != nil {
+ return nil, err
+ }
+
+ go args.Control(conn)
+ }
+
+ if exec.Interactive {
+ // Handle interactive sections
+ if args.Stdin != nil && args.Stdout != nil {
+ // Connect to the websocket
+ conn, err := r.GetOperationWebsocket(op.ID, fds["0"])
+ if err != nil {
+ return nil, err
+ }
+
+ // And attach stdin and stdout to it
+ go func() {
+ shared.WebsocketSendStream(conn, args.Stdin, -1)
+ <-shared.WebsocketRecvStream(args.Stdout, conn)
+ conn.Close()
+ }()
+ }
+ } else {
+ // Handle non-interactive sessions
+ dones := []chan bool{}
+ conns := []*websocket.Conn{}
+
+ // Handle stdin
+ if fds["0"] != "" {
+ conn, err := r.GetOperationWebsocket(op.ID, fds["0"])
+ if err != nil {
+ return nil, err
+ }
+
+ conns = append(conns, conn)
+ dones = append(dones, shared.WebsocketSendStream(conn, args.Stdin, -1))
+ }
+
+ // Handle stdout
+ if fds["1"] != "" {
+ conn, err := r.GetOperationWebsocket(op.ID, fds["1"])
+ if err != nil {
+ return nil, err
+ }
+
+ conns = append(conns, conn)
+ dones = append(dones, shared.WebsocketRecvStream(args.Stdout, conn))
+ }
+
+ // Handle stderr
+ if fds["2"] != "" {
+ conn, err := r.GetOperationWebsocket(op.ID, fds["2"])
+ if err != nil {
+ return nil, err
+ }
+
+ conns = append(conns, conn)
+ dones = append(dones, shared.WebsocketRecvStream(args.Stderr, conn))
+ }
+
+ // Wait for everything to be done
+ go func() {
+ for _, chDone := range dones {
+ <-chDone
+ }
+
+ if fds["0"] != "" {
+ args.Stdin.Close()
+ }
+
+ for _, conn := range conns {
+ conn.Close()
+ }
+ }()
+ }
+ }
+
+ return op, nil
+}
+
+// GetContainerFile retrieves the provided path from the container
+func (r *ProtocolLXD) GetContainerFile(containerName string, path string) (io.ReadCloser, *ContainerFileResponse, error) {
+ // Prepare the HTTP request
+ url := fmt.Sprintf("%s/1.0/containers/%s/files?path=%s", r.httpHost, containerName, path)
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // Set the user agent
+ if r.httpUserAgent != "" {
+ req.Header.Set("User-Agent", r.httpUserAgent)
+ }
+
+ // Send the request
+ resp, err := r.http.Do(req)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // Check the return value for a cleaner error
+ if resp.StatusCode != http.StatusOK {
+ return nil, nil, fmt.Errorf("Failed to fetch %s: %s", url, resp.Status)
+ }
+
+ // Parse the headers
+ uid, gid, mode, fileType, _ := shared.ParseLXDFileHeaders(resp.Header)
+ fileResp := ContainerFileResponse{
+ UID: uid,
+ GID: gid,
+ Mode: mode,
+ Type: fileType,
+ }
+
+ if fileResp.Type == "directory" {
+ // Decode the response
+ response := api.Response{}
+ decoder := json.NewDecoder(resp.Body)
+
+ err = decoder.Decode(&response)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // Get the file list
+ entries := []string{}
+ err = response.MetadataAsStruct(&entries)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ fileResp.Entries = entries
+
+ return nil, &fileResp, err
+ }
+
+ return resp.Body, &fileResp, err
+}
+
+// CreateContainerFile tells LXD to create a file in the container
+func (r *ProtocolLXD) CreateContainerFile(containerName string, path string, args ContainerFileArgs) error {
+ if args.Type == "directory" {
+ if !r.HasExtension("directory_manipulation") {
+ return fmt.Errorf("The server is missing the required \"directory_manipulation\" API extension")
+ }
+ }
+
+ if args.WriteMode == "append" {
+ if !r.HasExtension("file_append") {
+ return fmt.Errorf("The server is missing the required \"file_append\" API extension")
+ }
+ }
+
+ // Prepare the HTTP request
+ url := fmt.Sprintf("%s/1.0/containers/%s/files?path=%s", r.httpHost, containerName, path)
+ req, err := http.NewRequest("POST", url, args.Content)
+ if err != nil {
+ return err
+ }
+
+ // Set the user agent
+ if r.httpUserAgent != "" {
+ req.Header.Set("User-Agent", r.httpUserAgent)
+ }
+
+ // Set the various headers
+ req.Header.Set("X-LXD-uid", fmt.Sprintf("%d", args.UID))
+ req.Header.Set("X-LXD-gid", fmt.Sprintf("%d", args.GID))
+ req.Header.Set("X-LXD-mode", fmt.Sprintf("%04o", args.Mode))
+
+ if args.Type != "" {
+ req.Header.Set("X-LXD-type", args.Type)
+ }
+
+ if args.WriteMode != "" {
+ req.Header.Set("X-LXD-write", args.WriteMode)
+ }
+
+ // Send the request
+ resp, err := r.http.Do(req)
+ if err != nil {
+ return err
+ }
+
+ // Check the return value for a cleaner error
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("Failed to upload to %s: %s", url, resp.Status)
+ }
+
+ return nil
+}
+
+// DeleteContainerFile deletes a file in the container
+func (r *ProtocolLXD) DeleteContainerFile(containerName string, path string) error {
+ if !r.HasExtension("file_delete") {
+ return fmt.Errorf("The server is missing the required \"file_delete\" API extension")
+ }
+
+ // Send the request
+ _, _, err := r.query("DELETE", fmt.Sprintf("/containers/%s/files?path=%s", containerName, path), nil, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// GetContainerSnapshotNames returns a list of snapshot names for the container
+func (r *ProtocolLXD) GetContainerSnapshotNames(containerName string) ([]string, error) {
+ urls := []string{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", fmt.Sprintf("/containers/%s/snapshots", containerName), nil, "", &urls)
+ if err != nil {
+ return nil, err
+ }
+
+ // Parse it
+ names := []string{}
+ for _, url := range urls {
+ fields := strings.Split(url, fmt.Sprintf("/containers/%s/snapshots/", containerName))
+ names = append(names, fields[len(fields)-1])
+ }
+
+ return names, nil
+}
+
+// GetContainerSnapshots returns a list of snapshots for the container
+func (r *ProtocolLXD) GetContainerSnapshots(containerName string) ([]api.ContainerSnapshot, error) {
+ snapshots := []api.ContainerSnapshot{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", fmt.Sprintf("/containers/%s/snapshots?recursion=1", containerName), nil, "", snapshots)
+ if err != nil {
+ return nil, err
+ }
+
+ return snapshots, nil
+}
+
+// GetContainerSnapshot returns a Snapshot struct for the provided container and snapshot names
+func (r *ProtocolLXD) GetContainerSnapshot(containerName string, name string) (*api.ContainerSnapshot, string, error) {
+ snapshot := api.ContainerSnapshot{}
+
+ // Fetch the raw value
+ etag, err := r.queryStruct("GET", fmt.Sprintf("/containers/%s/snapshots/%s", containerName, name), nil, "", &snapshot)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return &snapshot, etag, nil
+}
+
+// CreateContainerSnapshot requests that LXD creates a new snapshot for the container
+func (r *ProtocolLXD) CreateContainerSnapshot(containerName string, snapshot api.ContainerSnapshotsPost) (*Operation, error) {
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("/containers/%s/snapshots", containerName), snapshot, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// RenameContainerSnapshot requests that LXD renames the snapshot
+func (r *ProtocolLXD) RenameContainerSnapshot(containerName string, name string, container api.ContainerSnapshotPost) (*Operation, error) {
+ // Sanity check
+ if container.Migration {
+ return nil, fmt.Errorf("Can't ask for a migration through RenameContainerSnapshot")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("/containers/%s/snapshots/%s", containerName, name), container, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// MigrateContainerSnapshot requests that LXD prepares for a snapshot migration
+func (r *ProtocolLXD) MigrateContainerSnapshot(containerName string, name string, container api.ContainerSnapshotPost) (*Operation, error) {
+ // Sanity check
+ if !container.Migration {
+ return nil, fmt.Errorf("Can't ask for a rename through MigrateContainerSnapshot")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("/containers/%s/snapshots/%s", containerName, name), container, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// DeleteContainerSnapshot requests that LXD deletes the container snapshot
+func (r *ProtocolLXD) DeleteContainerSnapshot(containerName string, name string) (*Operation, error) {
+ // Send the request
+ op, _, err := r.queryOperation("DELETE", fmt.Sprintf("/containers/%s/snapshots/%s", containerName, name), nil, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// GetContainerState returns a ContainerState entry for the provided container name
+func (r *ProtocolLXD) GetContainerState(name string) (*api.ContainerState, string, error) {
+ state := api.ContainerState{}
+
+ // Fetch the raw value
+ etag, err := r.queryStruct("GET", fmt.Sprintf("/containers/%s/state", name), nil, "", &state)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return &state, etag, nil
+}
+
+// UpdateContainerState updates the container to match the requested state
+func (r *ProtocolLXD) UpdateContainerState(name string, state api.ContainerStatePut, ETag string) (*Operation, error) {
+ // Send the request
+ op, _, err := r.queryOperation("PUT", fmt.Sprintf("/containers/%s/state", name), state, ETag)
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// GetContainerLogfiles returns a list of logfiles for the container
+func (r *ProtocolLXD) GetContainerLogfiles(name string) ([]string, error) {
+ urls := []string{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", fmt.Sprintf("/containers/%s/logs", name), nil, "", &urls)
+ if err != nil {
+ return nil, err
+ }
+
+ // Parse it
+ logfiles := []string{}
+ for _, url := range logfiles {
+ fields := strings.Split(url, fmt.Sprintf("/containers/%s/logs/", name))
+ logfiles = append(logfiles, fields[len(fields)-1])
+ }
+
+ return logfiles, nil
+}
+
+// GetContainerLogfile returns the content of the requested logfile
+//
+// Note that it's the caller's responsability to close the returned ReadCloser
+func (r *ProtocolLXD) GetContainerLogfile(name string, filename string) (io.ReadCloser, error) {
+ // Prepare the HTTP request
+ url := fmt.Sprintf("%s/1.0/containers/%s/logs/%s", r.httpHost, name, filename)
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ // Set the user agent
+ if r.httpUserAgent != "" {
+ req.Header.Set("User-Agent", r.httpUserAgent)
+ }
+
+ // Send the request
+ resp, err := r.http.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ // Check the return value for a cleaner error
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("Failed to fetch %s: %s", url, resp.Status)
+ }
+
+ return resp.Body, err
+}
+
+// DeleteContainerLogfile deletes the requested logfile
+func (r *ProtocolLXD) DeleteContainerLogfile(name string, filename string) error {
+ // Send the request
+ _, _, err := r.query("DELETE", fmt.Sprintf("/containers/%s/logs/%s", name, filename), nil, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/client/lxd_events.go b/client/lxd_events.go
new file mode 100644
index 0000000..e63e78b
--- /dev/null
+++ b/client/lxd_events.go
@@ -0,0 +1,105 @@
+package lxd
+
+import (
+ "encoding/json"
+
+ "github.com/lxc/lxd/shared"
+)
+
+// Event handling functions
+
+// GetEvents connects to the LXD monitoring interface
+func (r *ProtocolLXD) GetEvents() (*EventListener, error) {
+ // Prevent anything else from interacting with the listeners
+ r.eventListenersLock.Lock()
+ defer r.eventListenersLock.Unlock()
+
+ // Setup a new listener
+ listener := EventListener{
+ r: r,
+ chActive: make(chan bool),
+ }
+
+ if r.eventListeners != nil {
+ // There is an existing Go routine setup, so just add another target
+ r.eventListeners = append(r.eventListeners, &listener)
+ return &listener, nil
+ }
+
+ // Initialize the list if needed
+ r.eventListeners = []*EventListener{}
+
+ // Setup a new connection with LXD
+ conn, err := r.websocket("/events")
+ if err != nil {
+ return nil, err
+ }
+
+ // Add the listener
+ r.eventListeners = append(r.eventListeners, &listener)
+
+ // And spawn the listener
+ go func() {
+ for {
+ r.eventListenersLock.Lock()
+ if len(r.eventListeners) == 0 {
+ // We don't need the connection anymore, disconnect
+ conn.Close()
+
+ r.eventListeners = nil
+ r.eventListenersLock.Unlock()
+ break
+ }
+ r.eventListenersLock.Unlock()
+
+ _, data, err := conn.ReadMessage()
+ if err != nil {
+ // Prevent anything else from interacting with the listeners
+ r.eventListenersLock.Lock()
+ defer r.eventListenersLock.Unlock()
+
+ // Tell all the current listeners about the failure
+ for _, listener := range r.eventListeners {
+ listener.err = err
+ listener.disconnected = true
+ close(listener.chActive)
+ }
+
+ // And remove them all from the list
+ r.eventListeners = []*EventListener{}
+ return
+ }
+
+ // Attempt to unpack the message
+ message := make(map[string]interface{})
+ err = json.Unmarshal(data, &message)
+ if err != nil {
+ continue
+ }
+
+ // Extract the message type
+ _, ok := message["type"]
+ if !ok {
+ continue
+ }
+ messageType := message["type"].(string)
+
+ // Send the message to all handlers
+ r.eventListenersLock.Lock()
+ for _, listener := range r.eventListeners {
+ listener.targetsLock.Lock()
+ for _, target := range listener.targets {
+ if target.types != nil && !shared.StringInSlice(messageType, target.types) {
+ continue
+ }
+
+ go target.function(message)
+ }
+ listener.targetsLock.Unlock()
+ }
+ r.eventListenersLock.Unlock()
+ }
+ }()
+
+ return &listener, nil
+}
diff --git a/client/lxd_images.go b/client/lxd_images.go
new file mode 100644
index 0000000..6eb96db
--- /dev/null
+++ b/client/lxd_images.go
@@ -0,0 +1,389 @@
+package lxd
+
+import (
+ "crypto/sha256"
+ "fmt"
+ "io"
+ "mime"
+ "mime/multipart"
+ "net/http"
+ "strings"
+
+ "github.com/lxc/lxd/shared"
+ "github.com/lxc/lxd/shared/api"
+ "github.com/lxc/lxd/shared/ioprogress"
+)
+
+// Image handling functions
+
+// GetImages returns a list of available images as Image structs
+func (r *ProtocolLXD) GetImages() ([]api.Image, error) {
+ images := []api.Image{}
+
+ _, err := r.queryStruct("GET", "/images?recursion=1", nil, "", &images)
+ if err != nil {
+ return nil, err
+ }
+
+ return images, nil
+}
+
+// GetImageFingerprints returns a list of available image fingerprints
+func (r *ProtocolLXD) GetImageFingerprints() ([]string, error) {
+ urls := []string{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", "/images", nil, "", &urls)
+ if err != nil {
+ return nil, err
+ }
+
+ // Parse it
+ fingerprints := []string{}
+ for _, url := range urls {
+ fields := strings.Split(url, "/images/")
+ fingerprints = append(fingerprints, fields[len(fields)-1])
+ }
+
+ return fingerprints, nil
+}
+
+// GetImage returns an Image struct for the provided fingerprint
+func (r *ProtocolLXD) GetImage(fingerprint string) (*api.Image, string, error) {
+ return r.GetPrivateImage(fingerprint, "")
+}
+
+// GetImageFile downloads an image from the server, returning an ImageFileRequest struct
+func (r *ProtocolLXD) GetImageFile(fingerprint string, req ImageFileRequest) (*ImageFileResponse, error) {
+ return r.GetPrivateImageFile(fingerprint, "", req)
+}
+
+// GetPrivateImage is similar to GetImage but allows passing a secret download token
+func (r *ProtocolLXD) GetPrivateImage(fingerprint string, secret string) (*api.Image, string, error) {
+ image := api.Image{}
+
+ // Build the API path
+ path := fmt.Sprintf("/images/%s", fingerprint)
+ if secret != "" {
+ path = fmt.Sprintf("%s?secret=%s", path, secret)
+ }
+
+ // Fetch the raw value
+ etag, err := r.queryStruct("GET", path, nil, "", &image)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return &image, etag, nil
+}
+
+// GetPrivateImageFile is similar to GetImageFile but allows passing a secret download token
+func (r *ProtocolLXD) GetPrivateImageFile(fingerprint string, secret string, req ImageFileRequest) (*ImageFileResponse, error) {
+ // Sanity checks
+ if req.MetaFile == nil && req.RootfsFile == nil {
+ return nil, fmt.Errorf("No file requested")
+ }
+
+ // Prepare the response
+ resp := ImageFileResponse{}
+
+ // Build the URL
+ url := fmt.Sprintf("%s/1.0/images/%s/export", r.httpHost, fingerprint)
+ if secret != "" {
+ url = fmt.Sprintf("%s?secret=%s", url, secret)
+ }
+
+ // Prepare the download request
+ request, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ if r.httpUserAgent != "" {
+ request.Header.Set("User-Agent", r.httpUserAgent)
+ }
+
+ // Start the request
+ response, err := r.http.Do(request)
+ if err != nil {
+ return nil, err
+ }
+ defer response.Body.Close()
+
+ if response.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("Unable to fetch %s: %s", url, response.Status)
+ }
+
+ ctype, ctypeParams, err := mime.ParseMediaType(response.Header.Get("Content-Type"))
+ if err != nil {
+ ctype = "application/octet-stream"
+ }
+
+ // Handle the data
+ body := response.Body
+ if req.ProgressHandler != nil {
+ body = &ioprogress.ProgressReader{
+ ReadCloser: response.Body,
+ Tracker: &ioprogress.ProgressTracker{
+ Length: response.ContentLength,
+ Handler: func(percent int64, speed int64) {
+ req.ProgressHandler(ProgressData{Text: fmt.Sprintf("%d%% (%s/s)", percent, shared.GetByteSizeString(speed, 2))})
+ },
+ },
+ }
+ }
+
+ // Hashing
+ sha256 := sha256.New()
+
+ // Deal with split images
+ if ctype == "multipart/form-data" {
+ if req.MetaFile == nil || req.RootfsFile == nil {
+ return nil, fmt.Errorf("Multi-part image but only one target file provided")
+ }
+
+ // Parse the POST data
+ mr := multipart.NewReader(body, ctypeParams["boundary"])
+
+ // Get the metadata tarball
+ part, err := mr.NextPart()
+ if err != nil {
+ return nil, err
+ }
+
+ if part.FormName() != "metadata" {
+ return nil, fmt.Errorf("Invalid multipart image")
+ }
+
+ size, err := io.Copy(io.MultiWriter(req.MetaFile, sha256), part)
+ if err != nil {
+ return nil, err
+ }
+ resp.MetaSize = size
+ resp.MetaName = part.FileName()
+
+ // Get the rootfs tarball
+ part, err = mr.NextPart()
+ if err != nil {
+ return nil, err
+ }
+
+ if part.FormName() != "rootfs" {
+ return nil, fmt.Errorf("Invalid multipart image")
+ }
+
+ size, err = io.Copy(io.MultiWriter(req.RootfsFile, sha256), part)
+ if err != nil {
+ return nil, err
+ }
+ resp.RootfsSize = size
+ resp.RootfsName = part.FileName()
+
+ // Check the hash
+ hash := fmt.Sprintf("%x", sha256.Sum(nil))
+ if hash != fingerprint {
+ return nil, fmt.Errorf("Image fingerprint doesn't match. Got %s expected %s", hash, fingerprint)
+ }
+
+ return &resp, nil
+ }
+
+ // Deal with unified images
+ _, cdParams, err := mime.ParseMediaType(response.Header.Get("Content-Disposition"))
+ if err != nil {
+ return nil, err
+ }
+
+ filename, ok := cdParams["filename"]
+ if !ok {
+ return nil, fmt.Errorf("No filename in Content-Disposition header")
+ }
+
+ size, err := io.Copy(io.MultiWriter(req.MetaFile, sha256), body)
+ if err != nil {
+ return nil, err
+ }
+ resp.MetaSize = size
+ resp.MetaName = filename
+
+ // Check the hash
+ hash := fmt.Sprintf("%x", sha256.Sum(nil))
+ if hash != fingerprint {
+ return nil, fmt.Errorf("Image fingerprint doesn't match. Got %s expected %s", hash, fingerprint)
+ }
+
+ return &resp, nil
+}
+
+// GetImageAliases returns the list of available aliases as ImageAliasesEntry structs
+func (r *ProtocolLXD) GetImageAliases() ([]api.ImageAliasesEntry, error) {
+ aliases := []api.ImageAliasesEntry{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", "/images/aliases?recursion=1", nil, "", aliases)
+ if err != nil {
+ return nil, err
+ }
+
+ return aliases, nil
+}
+
+// GetImageAliasNames returns the list of available alias names
+func (r *ProtocolLXD) GetImageAliasNames() ([]string, error) {
+ urls := []string{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", "/images/aliases", nil, "", &urls)
+ if err != nil {
+ return nil, err
+ }
+
+ // Parse it
+ names := []string{}
+ for _, url := range urls {
+ fields := strings.Split(url, "/images/aliases/")
+ names = append(names, fields[len(fields)-1])
+ }
+
+ return names, nil
+}
+
+// GetImageAlias returns an existing alias as an ImageAliasesEntry struct
+func (r *ProtocolLXD) GetImageAlias(name string) (*api.ImageAliasesEntry, string, error) {
+ alias := api.ImageAliasesEntry{}
+
+ // Fetch the raw value
+ etag, err := r.queryStruct("GET", fmt.Sprintf("/images/aliases/%s", name), nil, "", &alias)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return &alias, etag, nil
+}
+
+// CreateImage requests that LXD creates, copies or import a new image
+func (r *ProtocolLXD) CreateImage(image api.ImagesPost) (*Operation, error) {
+ if image.CompressionAlgorithm != "" {
+ if !r.HasExtension("image_compression_algorithm") {
+ return nil, fmt.Errorf("The server is missing the required \"image_compression_algorithm\" API extension")
+ }
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", "/images", image, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// CopyImage copies an existing image to a remote server. Additional options can be passed using ImageCopyArgs
+func (r *ProtocolLXD) CopyImage(image api.Image, target ContainerServer, args *ImageCopyArgs) (*Operation, error) {
+ // Prepare the copy request
+ req := api.ImagesPost{
+ Source: &api.ImagesPostSource{
+ ImageSource: api.ImageSource{
+ Certificate: r.httpCertificate,
+ Protocol: "lxd",
+ Server: r.httpHost,
+ },
+ Fingerprint: image.Fingerprint,
+ Mode: "pull",
+ Type: "image",
+ },
+ }
+
+ // Process the arguments
+ if args != nil {
+ req.Aliases = args.Aliases
+ req.AutoUpdate = args.AutoUpdate
+ req.Public = args.Public
+
+ if args.CopyAliases {
+ req.Aliases = image.Aliases
+ if args.Aliases != nil {
+ req.Aliases = append(req.Aliases, args.Aliases...)
+ }
+ }
+ }
+
+ return target.CreateImage(req)
+}
+
+// UpdateImage updates the image definition
+func (r *ProtocolLXD) UpdateImage(fingerprint string, image api.ImagePut, ETag string) error {
+ // Send the request
+ _, _, err := r.query("PUT", fmt.Sprintf("/images/%s", fingerprint), image, ETag)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// DeleteImage requests that LXD removes an image from the store
+func (r *ProtocolLXD) DeleteImage(fingerprint string) (*Operation, error) {
+ // Send the request
+ op, _, err := r.queryOperation("DELETE", fmt.Sprintf("/images/%s", fingerprint), nil, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// CreateImageSecret requests that LXD issues a temporary image secret
+func (r *ProtocolLXD) CreateImageSecret(fingerprint string) (*Operation, error) {
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("/images/%s/secret", fingerprint), nil, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// CreateImageAlias sets up a new image alias
+func (r *ProtocolLXD) CreateImageAlias(alias api.ImageAliasesPost) error {
+ // Send the request
+ _, _, err := r.query("POST", "/images/aliases", alias, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// UpdateImageAlias updates the image alias definition
+func (r *ProtocolLXD) UpdateImageAlias(name string, alias api.ImageAliasesEntryPut, ETag string) error {
+ // Send the request
+ _, _, err := r.query("PUT", fmt.Sprintf("/images/aliases/%s", name), alias, ETag)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// RenameImageAlias renames an existing image alias
+func (r *ProtocolLXD) RenameImageAlias(name string, alias api.ImageAliasesEntryPost) error {
+ // Send the request
+ _, _, err := r.query("POST", fmt.Sprintf("/images/aliases/%s", name), alias, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// DeleteImageAlias removes an alias from the LXD image store
+func (r *ProtocolLXD) DeleteImageAlias(name string) error {
+ // Send the request
+ _, _, err := r.query("DELETE", fmt.Sprintf("/images/aliases/%s", name), nil, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/client/lxd_networks.go b/client/lxd_networks.go
new file mode 100644
index 0000000..cbc3fd3
--- /dev/null
+++ b/client/lxd_networks.go
@@ -0,0 +1,102 @@
+package lxd
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/lxc/lxd/shared/api"
+)
+
+// GetNetworkNames returns a list of network names
+func (r *ProtocolLXD) GetNetworkNames() ([]string, error) {
+ urls := []string{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", "/networks", nil, "", &urls)
+ if err != nil {
+ return nil, err
+ }
+
+ // Parse it
+ names := []string{}
+ for _, url := range urls {
+ fields := strings.Split(url, "/networks/")
+ names = append(names, fields[len(fields)-1])
+ }
+
+ return names, nil
+}
+
+// GetNetworks returns a list of Network struct
+func (r *ProtocolLXD) GetNetworks() ([]api.Network, error) {
+ networks := []api.Network{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", "/networks?recursion=1", nil, "", &networks)
+ if err != nil {
+ return nil, err
+ }
+
+ return networks, nil
+}
+
+// GetNetwork returns a Network entry for the provided name
+func (r *ProtocolLXD) GetNetwork(name string) (*api.Network, string, error) {
+ network := api.Network{}
+
+ // Fetch the raw value
+ etag, err := r.queryStruct("GET", fmt.Sprintf("/networks/%s", name), nil, "", &network)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return &network, etag, nil
+}
+
+// CreateNetwork defines a new network using the provided Network struct
+func (r *ProtocolLXD) CreateNetwork(network api.NetworksPost) error {
+ if !r.HasExtension("network") {
+ return fmt.Errorf("The server is missing the required \"network\" API extension")
+ }
+
+ // Send the request
+ _, _, err := r.query("POST", "/networks", network, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// UpdateNetwork updates the network to match the provided Network struct
+func (r *ProtocolLXD) UpdateNetwork(name string, network api.NetworkPut, ETag string) error {
+ // Send the request
+ _, _, err := r.query("PUT", fmt.Sprintf("/networks/%s", name), network, ETag)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// RenameNetwork renames an existing network entry
+func (r *ProtocolLXD) RenameNetwork(name string, network api.NetworkPost) error {
+ // Send the request
+ _, _, err := r.query("POST", fmt.Sprintf("/networks/%s", name), network, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// DeleteNetwork deletes an existing network
+func (r *ProtocolLXD) DeleteNetwork(name string) error {
+ // Send the request
+ _, _, err := r.query("DELETE", fmt.Sprintf("/networks/%s", name), nil, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/client/lxd_operations.go b/client/lxd_operations.go
new file mode 100644
index 0000000..fe39c02
--- /dev/null
+++ b/client/lxd_operations.go
@@ -0,0 +1,43 @@
+package lxd
+
+import (
+ "fmt"
+
+ "github.com/gorilla/websocket"
+
+ "github.com/lxc/lxd/shared/api"
+)
+
+// GetOperation returns an Operation entry for the provided uuid
+func (r *ProtocolLXD) GetOperation(uuid string) (*api.Operation, string, error) {
+ op := api.Operation{}
+
+ // Fetch the raw value
+ etag, err := r.queryStruct("GET", fmt.Sprintf("/operations/%s", uuid), nil, "", &op)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return &op, etag, nil
+}
+
+// GetOperationWebsocket returns a websocket connection for the provided operation
+func (r *ProtocolLXD) GetOperationWebsocket(uuid string, secret string) (*websocket.Conn, error) {
+ path := fmt.Sprintf("/operations/%s/websocket", uuid)
+ if secret != "" {
+ path = fmt.Sprintf("%s?secret=%s", path, secret)
+ }
+
+ return r.websocket(path)
+}
+
+// DeleteOperation deletes (cancels) a running operation
+func (r *ProtocolLXD) DeleteOperation(uuid string) error {
+ // Send the request
+ _, _, err := r.query("DELETE", fmt.Sprintf("/operations/%s", uuid), nil, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/client/lxd_profiles.go b/client/lxd_profiles.go
new file mode 100644
index 0000000..a11f4cb
--- /dev/null
+++ b/client/lxd_profiles.go
@@ -0,0 +1,100 @@
+package lxd
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/lxc/lxd/shared/api"
+)
+
+// Profile handling functions
+
+// GetProfileNames returns a list of available profile names
+func (r *ProtocolLXD) GetProfileNames() ([]string, error) {
+ urls := []string{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", "/profiles", nil, "", &urls)
+ if err != nil {
+ return nil, err
+ }
+
+ // Parse it
+ names := []string{}
+ for _, url := range urls {
+ fields := strings.Split(url, "/profiles/")
+ names = append(names, fields[len(fields)-1])
+ }
+
+ return names, nil
+}
+
+// GetProfiles returns a list of available Profile structs
+func (r *ProtocolLXD) GetProfiles() ([]api.Profile, error) {
+ profiles := []api.Profile{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", "/profiles?recursion=1", nil, "", &profiles)
+ if err != nil {
+ return nil, err
+ }
+
+ return profiles, nil
+}
+
+// GetProfile returns a Profile entry for the provided name
+func (r *ProtocolLXD) GetProfile(name string) (*api.Profile, string, error) {
+ profile := api.Profile{}
+
+ // Fetch the raw value
+ etag, err := r.queryStruct("GET", fmt.Sprintf("/profiles/%s", name), nil, "", &profile)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return &profile, etag, nil
+}
+
+// CreateProfile defines a new container profile
+func (r *ProtocolLXD) CreateProfile(profile api.ProfilesPost) error {
+ // Send the request
+ _, _, err := r.query("POST", "/profiles", profile, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// UpdateProfile updates the profile to match the provided Profile struct
+func (r *ProtocolLXD) UpdateProfile(name string, profile api.ProfilePut, ETag string) error {
+ // Send the request
+ _, _, err := r.query("PUT", fmt.Sprintf("/profiles/%s", name), profile, ETag)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// RenameProfile renames an existing profile entry
+func (r *ProtocolLXD) RenameProfile(name string, profile api.ProfilePost) error {
+ // Send the request
+ _, _, err := r.query("POST", fmt.Sprintf("/profiles/%s", name), profile, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// DeleteProfile deletes a profile
+func (r *ProtocolLXD) DeleteProfile(name string) error {
+ // Send the request
+ _, _, err := r.query("DELETE", fmt.Sprintf("/profiles/%s", name), nil, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/client/lxd_server.go b/client/lxd_server.go
new file mode 100644
index 0000000..fbae67f
--- /dev/null
+++ b/client/lxd_server.go
@@ -0,0 +1,50 @@
+package lxd
+
+import (
+ "github.com/lxc/lxd/shared/api"
+)
+
+// Server handling functions
+
+// GetServer returns the server status as a Server struct
+func (r *ProtocolLXD) GetServer() (*api.Server, string, error) {
+ server := api.Server{}
+
+ // Return the cached entry if present
+ if r.server != nil {
+ return r.server, "", nil
+ }
+
+ // Fetch the raw value
+ etag, err := r.queryStruct("GET", "", nil, "", &server)
+ if err != nil {
+ return nil, "", err
+ }
+
+ // Add the value to the cache
+ r.server = &server
+
+ return &server, etag, nil
+}
+
+// UpdateServer updates the server status to match the provided Server struct
+func (r *ProtocolLXD) UpdateServer(server api.ServerPut, ETag string) error {
+ // Send the request
+ _, _, err := r.query("PUT", "", server, ETag)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// HasExtension returns true if the server supports a given API extension
+func (r *ProtocolLXD) HasExtension(extension string) bool {
+ for _, entry := range r.server.APIExtensions {
+ if entry == extension {
+ return true
+ }
+ }
+
+ return false
+}
diff --git a/client/lxd_storage_pools.go b/client/lxd_storage_pools.go
new file mode 100644
index 0000000..fb49058
--- /dev/null
+++ b/client/lxd_storage_pools.go
@@ -0,0 +1,97 @@
+package lxd
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/lxc/lxd/shared/api"
+)
+
+// Storage pool handling functions
+
+// GetStoragePoolNames returns the names of all storage pools
+func (r *ProtocolLXD) GetStoragePoolNames() ([]string, error) {
+ if !r.HasExtension("storage") {
+ return nil, fmt.Errorf("The server is missing the required \"storage\" API extension")
+ }
+
+ urls := []string{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", "/storage-pools", nil, "", &urls)
+ if err != nil {
+ return nil, err
+ }
+
+ // Parse it
+ names := []string{}
+ for _, url := range urls {
+ fields := strings.Split(url, "/storage-pools/")
+ names = append(names, fields[len(fields)-1])
+ }
+
+ return names, nil
+}
+
+// GetStoragePools returns a list of StoragePool entries
+func (r *ProtocolLXD) GetStoragePools() ([]api.StoragePool, error) {
+ pools := []api.StoragePool{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", "/storage-pools?recursion=1", nil, "", &pools)
+ if err != nil {
+ return nil, err
+ }
+
+ return pools, nil
+}
+
+// GetStoragePool returns a StoragePool entry for the provided pool name
+func (r *ProtocolLXD) GetStoragePool(name string) (*api.StoragePool, string, error) {
+ pool := api.StoragePool{}
+
+ // Fetch the raw value
+ etag, err := r.queryStruct("GET", fmt.Sprintf("/storage-pools/%s", name), nil, "", &pool)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return &pool, etag, nil
+}
+
+// CreateStoragePool defines a new storage pool using the provided StoragePool struct
+func (r *ProtocolLXD) CreateStoragePool(pool api.StoragePoolsPost) error {
+ if !r.HasExtension("storage") {
+ return fmt.Errorf("The server is missing the required \"storage\" API extension")
+ }
+
+ // Send the request
+ _, _, err := r.query("POST", "/storage-pools", pool, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// UpdateStoragePool updates the pool to match the provided StoragePool struct
+func (r *ProtocolLXD) UpdateStoragePool(name string, pool api.StoragePoolPut, ETag string) error {
+ // Send the request
+ _, _, err := r.query("PUT", fmt.Sprintf("/storage-pools/%s", name), pool, ETag)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// DeleteStoragePool deletes a storage pool
+func (r *ProtocolLXD) DeleteStoragePool(name string) error {
+ // Send the request
+ _, _, err := r.query("DELETE", fmt.Sprintf("/storage-pools/%s", name), nil, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/client/lxd_storage_volumes.go b/client/lxd_storage_volumes.go
new file mode 100644
index 0000000..980b9c3
--- /dev/null
+++ b/client/lxd_storage_volumes.go
@@ -0,0 +1,93 @@
+package lxd
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/lxc/lxd/shared/api"
+)
+
+// Storage volumes handling function
+
+// GetStoragePoolVolumeNames returns the names of all volumes in a pool
+func (r *ProtocolLXD) GetStoragePoolVolumeNames(pool string) ([]string, error) {
+ if !r.HasExtension("storage") {
+ return nil, fmt.Errorf("The server is missing the required \"storage\" API extension")
+ }
+
+ urls := []string{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", fmt.Sprintf("/storage-pools/%s/volumes", pool), nil, "", &urls)
+ if err != nil {
+ return nil, err
+ }
+
+ // Parse it
+ names := []string{}
+ for _, url := range urls {
+ fields := strings.Split(url, fmt.Sprintf("/storage-pools/%s/volumes/", pool))
+ names = append(names, fields[len(fields)-1])
+ }
+
+ return names, nil
+}
+
+// GetStoragePoolVolumes returns a list of StorageVolume entries for the provided pool
+func (r *ProtocolLXD) GetStoragePoolVolumes(pool string) ([]api.StorageVolume, error) {
+ volumes := []api.StorageVolume{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", fmt.Sprintf("/storage-pools/%s/volumes?recursion=1", pool), nil, "", &volumes)
+ if err != nil {
+ return nil, err
+ }
+
+ return volumes, nil
+}
+
+// GetStoragePoolVolume returns a StorageVolume entry for the provided pool and volume name
+func (r *ProtocolLXD) GetStoragePoolVolume(pool string, name string) (*api.StorageVolume, string, error) {
+ volume := api.StorageVolume{}
+
+ // Fetch the raw value
+ etag, err := r.queryStruct("GET", fmt.Sprintf("/storage-pools/%s/volumes/%s", pool, name), nil, "", &volume)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return &volume, etag, nil
+}
+
+// CreateStoragePoolVolume defines a new storage volume
+func (r *ProtocolLXD) CreateStoragePoolVolume(pool string, volume api.StorageVolumesPost) error {
+ // Send the request
+ _, _, err := r.query("POST", fmt.Sprintf("/storage-pools/%s/volumes", pool), volume, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// UpdateStoragePoolVolume updates the volume to match the provided StoragePoolVolume struct
+func (r *ProtocolLXD) UpdateStoragePoolVolume(pool string, name string, volume api.StorageVolumePut, ETag string) error {
+ // Send the request
+ _, _, err := r.query("PUT", fmt.Sprintf("/storage-pools/%s/volumes/%s", pool, name), volume, ETag)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// DeleteStoragePoolVolume deletes a storage pool
+func (r *ProtocolLXD) DeleteStoragePoolVolume(pool string, name string) error {
+ // Send the request
+ _, _, err := r.query("DELETE", fmt.Sprintf("/storage-pools/%s/volumes/%s", pool, name), nil, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/client/operations.go b/client/operations.go
new file mode 100644
index 0000000..3aedf80
--- /dev/null
+++ b/client/operations.go
@@ -0,0 +1,221 @@
+package lxd
+
+import (
+ "encoding/json"
+ "fmt"
+ "sync"
+
+ "github.com/gorilla/websocket"
+
+ "github.com/lxc/lxd/shared/api"
+)
+
+// The Operation type represents an ongoing LXD operation (asynchronous processing)
+type Operation struct {
+ api.Operation
+
+ r *ProtocolLXD
+ listener *EventListener
+ listenerLock sync.Mutex
+ chActive chan bool
+}
+
+// AddHandler adds a function to be called whenever an event is received
+func (op *Operation) AddHandler(function func(api.Operation)) (*EventTarget, error) {
+ // Make sure we have a listener setup
+ err := op.setupListener()
+ if err != nil {
+ return nil, err
+ }
+
+ // Make sure we're not racing with ourselves
+ op.listenerLock.Lock()
+ defer op.listenerLock.Unlock()
+
+ // If we're done already, just return
+ if op.StatusCode.IsFinal() {
+ return nil, nil
+ }
+
+ // Wrap the function to filter unwanted messages
+ wrapped := func(data interface{}) {
+ newOp := op.extractOperation(data)
+ if newOp == nil {
+ return
+ }
+
+ function(*newOp)
+ }
+
+ return op.listener.AddHandler([]string{"operation"}, wrapped)
+}
+
+// Cancel will request that LXD cancels the operation (if supported)
+func (op *Operation) Cancel() error {
+ return op.r.DeleteOperation(op.ID)
+}
+
+// GetWebsocket returns a raw websocket connection from the operation
+func (op *Operation) GetWebsocket(secret string) (*websocket.Conn, error) {
+ return op.r.GetOperationWebsocket(op.ID, secret)
+}
+
+// RemoveHandler removes a function to be called whenever an event is received
+func (op *Operation) RemoveHandler(target *EventTarget) error {
+ // Make sure we're not racing with ourselves
+ op.listenerLock.Lock()
+ defer op.listenerLock.Unlock()
+
+ // If the listener is gone, just return
+ if op.listener == nil {
+ return nil
+ }
+
+ return op.listener.RemoveHandler(target)
+}
+
+// Refresh pulls the current version of the operation and updates the struct
+func (op *Operation) Refresh() error {
+ // Don't bother with a manual update if we are listening for events
+ if op.listener != nil {
+ return nil
+ }
+
+ // Get the current version of the operation
+ newOp, _, err := op.r.GetOperation(op.ID)
+ if err != nil {
+ return err
+ }
+
+ // Update the operation struct
+ op.Operation = *newOp
+
+ return nil
+}
+
+// Wait lets you wait until the operation reaches a final state
+func (op *Operation) Wait() error {
+ // Check if not done already
+ if op.StatusCode.IsFinal() {
+ if op.Err != "" {
+ return fmt.Errorf(op.Err)
+ }
+
+ return nil
+ }
+
+ // Make sure we have a listener setup
+ err := op.setupListener()
+ if err != nil {
+ return err
+ }
+
+ <-op.chActive
+
+ // We're done, parse the result
+ if op.Err != "" {
+ return fmt.Errorf(op.Err)
+ }
+
+ return nil
+}
+
+func (op *Operation) setupListener() error {
+ // Make sure we're not racing with ourselves
+ op.listenerLock.Lock()
+ defer op.listenerLock.Unlock()
+
+ // We already have a listener setup
+ if op.listener != nil {
+ return nil
+ }
+
+ // Get a new listener
+ listener, err := op.r.GetEvents()
+ if err != nil {
+ return err
+ }
+
+ // Setup the handler
+ chReady := make(chan bool)
+ _, err = listener.AddHandler([]string{"operation"}, func(data interface{}) {
+ <-chReady
+
+ // Get an operation struct out of this data
+ newOp := op.extractOperation(data)
+ if newOp == nil {
+ return
+ }
+
+ // Update the struct
+ op.Operation = *newOp
+
+ // And check if we're done
+ if op.StatusCode.IsFinal() {
+ // Make sure we're not racing with ourselves
+ op.listenerLock.Lock()
+ defer op.listenerLock.Unlock()
+
+ op.listener.Disconnect()
+ op.listener = nil
+ close(op.chActive)
+ return
+ }
+ })
+ if err != nil {
+ listener.Disconnect()
+ return err
+ }
+
+ // And do a manual refresh to avoid races
+ err = op.Refresh()
+ if err != nil {
+ listener.Disconnect()
+ return err
+ }
+
+ // Check if not done already
+ if op.StatusCode.IsFinal() {
+ listener.Disconnect()
+ close(op.chActive)
+
+ if op.Err != "" {
+ return fmt.Errorf(op.Err)
+ }
+
+ return nil
+ }
+
+ // Start processing background updates
+ op.listener = listener
+ close(chReady)
+
+ return nil
+}
+
+func (op *Operation) extractOperation(data interface{}) *api.Operation {
+ // Extract the metadata
+ meta, ok := data.(map[string]interface{})["metadata"]
+ if !ok {
+ return nil
+ }
+
+ // And attempt to decode it as JSON operation data
+ encoded, err := json.Marshal(meta)
+ if err != nil {
+ return nil
+ }
+
+ newOp := api.Operation{}
+ err = json.Unmarshal(encoded, &newOp)
+ if err != nil {
+ return nil
+ }
+
+ // And now check that it's what we want
+ if newOp.ID != op.ID {
+ return nil
+ }
+
+ return &newOp
+}
diff --git a/client/simplestreams.go b/client/simplestreams.go
new file mode 100644
index 0000000..a69a2ea
--- /dev/null
+++ b/client/simplestreams.go
@@ -0,0 +1,17 @@
+package lxd
+
+import (
+ "net/http"
+
+ "github.com/lxc/lxd/shared/simplestreams"
+)
+
+// ProtocolSimpleStreams implements a SimpleStreams API client
+type ProtocolSimpleStreams struct {
+ ssClient *simplestreams.SimpleStreams
+
+ http *http.Client
+ httpHost string
+ httpUserAgent string
+ httpCertificate string
+}
diff --git a/client/simplestreams_images.go b/client/simplestreams_images.go
new file mode 100644
index 0000000..fc40f02
--- /dev/null
+++ b/client/simplestreams_images.go
@@ -0,0 +1,177 @@
+package lxd
+
+import (
+ "fmt"
+ "github.com/lxc/lxd/shared/api"
+ "strings"
+)
+
+// Image handling functions
+
+// GetImages returns a list of available images as Image structs
+func (r *ProtocolSimpleStreams) GetImages() ([]api.Image, error) {
+ return r.ssClient.ListImages()
+}
+
+// GetImageFingerprints returns a list of available image fingerprints
+func (r *ProtocolSimpleStreams) GetImageFingerprints() ([]string, error) {
+ // Get all the images from simplestreams
+ images, err := r.ssClient.ListImages()
+ if err != nil {
+ return nil, err
+ }
+
+ // And now extract just the fingerprints
+ fingerprints := []string{}
+ for _, img := range images {
+ fingerprints = append(fingerprints, img.Fingerprint)
+ }
+
+ return fingerprints, nil
+}
+
+// GetImage returns an Image struct for the provided fingerprint
+func (r *ProtocolSimpleStreams) GetImage(fingerprint string) (*api.Image, string, error) {
+ image, err := r.ssClient.GetImage(fingerprint)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return image, "", err
+}
+
+// GetImageFile downloads an image from the server, returning an ImageFileResponse struct
+func (r *ProtocolSimpleStreams) GetImageFile(fingerprint string, req ImageFileRequest) (*ImageFileResponse, error) {
+ // Sanity checks
+ if req.MetaFile == nil && req.RootfsFile == nil {
+ return nil, fmt.Errorf("No file requested")
+ }
+
+ // Get the file list
+ files, err := r.ssClient.GetFiles(fingerprint)
+ if err != nil {
+ return nil, err
+ }
+
+ // Prepare the response
+ resp := ImageFileResponse{}
+
+ // Download the LXD image file
+ meta, ok := files["meta"]
+ if ok && req.MetaFile != nil {
+ // Try over http
+ url := fmt.Sprintf("http://%s/%s", strings.TrimPrefix(r.httpHost, "https://"), meta.Path)
+
+ size, err := downloadFileSha256(r.http, r.httpUserAgent, req.ProgressHandler, "metadata", url, meta.Sha256, req.MetaFile)
+ if err != nil {
+ // Try over https
+ url = fmt.Sprintf("%s/%s", r.httpHost, meta.Path)
+ size, err = downloadFileSha256(r.http, r.httpUserAgent, req.ProgressHandler, "metadata", url, meta.Sha256, req.MetaFile)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ parts := strings.Split(meta.Path, "/")
+ resp.MetaName = parts[len(parts)-1]
+ resp.MetaSize = size
+ }
+
+ // Download the rootfs
+ rootfs, ok := files["root"]
+ if ok && req.RootfsFile != nil {
+ // Try over http
+ url := fmt.Sprintf("http://%s/%s", strings.TrimPrefix(r.httpHost, "https://"), rootfs.Path)
+
+ size, err := downloadFileSha256(r.http, r.httpUserAgent, req.ProgressHandler, "rootfs", url, rootfs.Sha256, req.RootfsFile)
+ if err != nil {
+ // Try over https
+ url = fmt.Sprintf("%s/%s", r.httpHost, rootfs.Path)
+ size, err = downloadFileSha256(r.http, r.httpUserAgent, req.ProgressHandler, "rootfs", url, rootfs.Sha256, req.RootfsFile)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ parts := strings.Split(rootfs.Path, "/")
+ resp.RootfsName = parts[len(parts)-1]
+ resp.RootfsSize = size
+ }
+
+ return &resp, nil
+}
+
+// GetPrivateImage isn't relevant for the simplestreams protocol
+func (r *ProtocolSimpleStreams) GetPrivateImage(fingerprint string, secret string) (*api.Image, string, error) {
+ return nil, "", fmt.Errorf("Private images aren't supported by the simplestreams protocol")
+}
+
+// GetPrivateImageFile isn't relevant for the simplestreams protocol
+func (r *ProtocolSimpleStreams) GetPrivateImageFile(fingerprint string, secret string, req ImageFileRequest) (*ImageFileResponse, error) {
+ return nil, fmt.Errorf("Private images aren't supported by the simplestreams protocol")
+}
+
+// GetImageAliases returns the list of available aliases as ImageAliasesEntry structs
+func (r *ProtocolSimpleStreams) GetImageAliases() ([]api.ImageAliasesEntry, error) {
+ return r.ssClient.ListAliases()
+}
+
+// GetImageAliasNames returns the list of available alias names
+func (r *ProtocolSimpleStreams) GetImageAliasNames() ([]string, error) {
+ // Get all the images from simplestreams
+ aliases, err := r.ssClient.ListAliases()
+ if err != nil {
+ return nil, err
+ }
+
+ // And now extract just the names
+ names := []string{}
+ for _, alias := range aliases {
+ names = append(names, alias.Name)
+ }
+
+ return names, nil
+}
+
+// GetImageAlias returns an existing alias as an ImageAliasesEntry struct
+func (r *ProtocolSimpleStreams) GetImageAlias(name string) (*api.ImageAliasesEntry, string, error) {
+ alias, err := r.ssClient.GetAlias(name)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return alias, "", err
+}
+
+// CopyImage copies an existing image to a remote server. Additional options can be passed using ImageCopyArgs
+func (r *ProtocolSimpleStreams) CopyImage(image api.Image, target ContainerServer, args *ImageCopyArgs) (*Operation, error) {
+ // Prepare the copy request
+ req := api.ImagesPost{
+ Source: &api.ImagesPostSource{
+ ImageSource: api.ImageSource{
+ Certificate: r.httpCertificate,
+ Protocol: "simplestreams",
+ Server: r.httpHost,
+ },
+ Fingerprint: image.Fingerprint,
+ Mode: "pull",
+ Type: "image",
+ },
+ }
+
+ // Process the arguments
+ if args != nil {
+ req.Aliases = args.Aliases
+ req.AutoUpdate = args.AutoUpdate
+ req.Public = args.Public
+
+ if args.CopyAliases {
+ req.Aliases = image.Aliases
+ if args.Aliases != nil {
+ req.Aliases = append(req.Aliases, args.Aliases...)
+ }
+ }
+ }
+
+ return target.CreateImage(req)
+}
diff --git a/client/util.go b/client/util.go
new file mode 100644
index 0000000..7aad619
--- /dev/null
+++ b/client/util.go
@@ -0,0 +1,145 @@
+package lxd
+
+import (
+ "crypto/sha256"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "net/url"
+
+ "github.com/lxc/lxd/shared"
+ "github.com/lxc/lxd/shared/ioprogress"
+)
+
+func tlsHTTPClient(tlsClientCert string, tlsClientKey string, tlsCA string, tlsServerCert string, proxy func(req *http.Request) (*url.URL, error)) (*http.Client, error) {
+ // Get the TLS configuration
+ tlsConfig, err := shared.GetTLSConfigMem(tlsClientCert, tlsClientKey, tlsCA, tlsServerCert)
+ if err != nil {
+ return nil, err
+ }
+
+ // Define the http transport
+ transport := &http.Transport{
+ TLSClientConfig: tlsConfig,
+ Dial: shared.RFC3493Dialer,
+ Proxy: shared.ProxyFromEnvironment,
+ DisableKeepAlives: true,
+ }
+
+ // Allow overriding the proxy
+ if proxy != nil {
+ transport.Proxy = proxy
+ }
+
+ // Define the http client
+ client := http.Client{
+ Transport: transport,
+ }
+
+ // Setup redirect policy
+ client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
+ // Replicate the headers
+ req.Header = via[len(via)-1].Header
+
+ return nil
+ }
+
+ return &client, nil
+}
+
+func unixHTTPClient(path string) (*http.Client, error) {
+ // Setup a Unix socket dialer
+ unixDial := func(network, addr string) (net.Conn, error) {
+ raddr, err := net.ResolveUnixAddr("unix", path)
+ if err != nil {
+ return nil, err
+ }
+
+ return net.DialUnix("unix", nil, raddr)
+ }
+
+ // Define the http transport
+ transport := &http.Transport{
+ Dial: unixDial,
+ DisableKeepAlives: true,
+ }
+
+ // Define the http client
+ client := http.Client{
+ Transport: transport,
+ }
+
+ // Setup redirect policy
+ client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
+ // Replicate the headers
+ req.Header = via[len(via)-1].Header
+
+ return nil
+ }
+
+ return &client, nil
+}
+
+func downloadFileSha256(httpClient *http.Client, useragent string, progress func(progress ProgressData), filename string, url string, hash string, target io.WriteSeeker) (int64, error) {
+ // Always seek to the beginning
+ target.Seek(0, io.SeekStart)
+
+ // Prepare the download request
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return -1, err
+ }
+
+ if useragent != "" {
+ req.Header.Set("User-Agent", useragent)
+ }
+
+ // Start the request
+ r, err := httpClient.Do(req)
+ if err != nil {
+ return -1, err
+ }
+ defer r.Body.Close()
+
+ if r.StatusCode != http.StatusOK {
+ return -1, fmt.Errorf("Unable to fetch %s: %s", url, r.Status)
+ }
+
+ // Handle the data
+ body := r.Body
+ if progress != nil {
+ body = &ioprogress.ProgressReader{
+ ReadCloser: r.Body,
+ Tracker: &ioprogress.ProgressTracker{
+ Length: r.ContentLength,
+ Handler: func(percent int64, speed int64) {
+ if filename != "" {
+ progress(ProgressData{Text: fmt.Sprintf("%s: %d%% (%s/s)", filename, percent, shared.GetByteSizeString(speed, 2))})
+ } else {
+ progress(ProgressData{Text: fmt.Sprintf("%d%% (%s/s)", percent, shared.GetByteSizeString(speed, 2))})
+ }
+ },
+ },
+ }
+ }
+
+ sha256 := sha256.New()
+ size, err := io.Copy(io.MultiWriter(target, sha256), body)
+ if err != nil {
+ return -1, err
+ }
+
+ result := fmt.Sprintf("%x", sha256.Sum(nil))
+ if result != hash {
+ return -1, fmt.Errorf("Hash mismatch for %s: %s != %s", url, result, hash)
+ }
+
+ return size, nil
+}
+
+type nullReadWriteCloser int
+
+func (nullReadWriteCloser) Close() error { return nil }
+func (nullReadWriteCloser) Write(p []byte) (int, error) { return len(p), nil }
+func (nullReadWriteCloser) Read(p []byte) (int, error) { return 0, io.EOF }
From d5d4044107fd9606dd8c02683deae9bf2d8f7bc7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Fri, 31 Mar 2017 18:30:39 -0400
Subject: [PATCH 02/15] lxc/config: Add new config handling code
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This isn't used for the client itself yet, but is used by lxd-benchmark
and can similarly be used by anyone who wants to use the same remote
syntax as our client supports.
Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
lxc/config/config.go | 39 ++++++++++++++
lxc/config/default.go | 52 +++++++++++++++++++
lxc/config/file.go | 73 ++++++++++++++++++++++++++
lxc/config/remote.go | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 303 insertions(+)
create mode 100644 lxc/config/config.go
create mode 100644 lxc/config/default.go
create mode 100644 lxc/config/file.go
create mode 100644 lxc/config/remote.go
diff --git a/lxc/config/config.go b/lxc/config/config.go
new file mode 100644
index 0000000..7e5f3df
--- /dev/null
+++ b/lxc/config/config.go
@@ -0,0 +1,39 @@
+package config
+
+import (
+ "fmt"
+ "path/filepath"
+)
+
+// Config holds settings to be used by a client or daemon
+type Config struct {
+ // DefaultRemote holds the remote daemon name from the Remotes map
+ // that the client should communicate with by default
+ DefaultRemote string `yaml:"default-remote"`
+
+ // Remotes defines a map of remote daemon names to the details for
+ // communication with the named daemon
+ Remotes map[string]Remote `yaml:"remotes"`
+
+ // Command line aliases for `lxc`
+ Aliases map[string]string `yaml:"aliases"`
+
+ // Configuration directory
+ ConfigDir string `yaml:"-"`
+
+ // The UserAgent to pass for all queries
+ UserAgent string `yaml:"-"`
+}
+
+// ConfigPath returns a joined path of the configuration directory and passed arguments
+func (c *Config) ConfigPath(paths ...string) string {
+ path := []string{c.ConfigDir}
+ path = append(path, paths...)
+
+ return filepath.Join(path...)
+}
+
+// ServerCertPath returns the path for the remote's server certificate
+func (c *Config) ServerCertPath(remote string) string {
+ return c.ConfigPath("servercerts", fmt.Sprintf("%s.crt", remote))
+}
diff --git a/lxc/config/default.go b/lxc/config/default.go
new file mode 100644
index 0000000..b16af97
--- /dev/null
+++ b/lxc/config/default.go
@@ -0,0 +1,52 @@
+package config
+
+// LocalRemote is the default local remote (over the LXD unix socket)
+var LocalRemote = Remote{
+ Addr: "unix://",
+ Static: true,
+ Public: false,
+}
+
+// ImagesRemote is the community image server (over simplestreams)
+var ImagesRemote = Remote{
+ Addr: "https://images.linuxcontainers.org",
+ Public: true,
+ Protocol: "simplestreams",
+}
+
+// UbuntuRemote is the Ubuntu image server (over simplestreams)
+var UbuntuRemote = Remote{
+ Addr: "https://cloud-images.ubuntu.com/releases",
+ Static: true,
+ Public: true,
+ Protocol: "simplestreams",
+}
+
+// UbuntuDailyRemote is the Ubuntu daily image server (over simplestreams)
+var UbuntuDailyRemote = Remote{
+ Addr: "https://cloud-images.ubuntu.com/daily",
+ Static: true,
+ Public: true,
+ Protocol: "simplestreams",
+}
+
+// StaticRemotes is the list of remotes which can't be removed
+var StaticRemotes = map[string]Remote{
+ "local": LocalRemote,
+ "ubuntu": UbuntuRemote,
+ "ubuntu-daily": UbuntuDailyRemote,
+}
+
+// DefaultRemotes is the list of default remotes
+var DefaultRemotes = map[string]Remote{
+ "images": ImagesRemote,
+ "local": LocalRemote,
+ "ubuntu": UbuntuRemote,
+ "ubuntu-daily": UbuntuDailyRemote,
+}
+
+// DefaultConfig is the default configuration
+var DefaultConfig = Config{
+ Remotes: DefaultRemotes,
+ DefaultRemote: "local",
+}
diff --git a/lxc/config/file.go b/lxc/config/file.go
new file mode 100644
index 0000000..220f29b
--- /dev/null
+++ b/lxc/config/file.go
@@ -0,0 +1,73 @@
+package config
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ "gopkg.in/yaml.v2"
+
+ "github.com/lxc/lxd/shared"
+)
+
+// LoadConfig reads the configuration from the config path; if the path does
+// not exist, it returns a default configuration.
+func LoadConfig(path string) (*Config, error) {
+ // Open the config file
+ content, err := ioutil.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to read the configuration file: %v", err)
+ }
+
+ // Decode the yaml document
+ c := Config{}
+ err = yaml.Unmarshal(content, &c)
+ if err != nil {
+ return nil, fmt.Errorf("Unable to decode the configuration: %v", err)
+ }
+
+ // Set default values
+ if c.Remotes == nil {
+ c.Remotes = make(map[string]Remote)
+ }
+ c.ConfigDir = filepath.Dir(path)
+
+ // Apply the static remotes
+ for k, v := range StaticRemotes {
+ c.Remotes[k] = v
+ }
+
+ return &c, nil
+}
+
+// SaveConfig writes the provided configuration to the config file.
+func (c *Config) SaveConfig(path string) error {
+ // Create a new copy for the config file
+ conf := Config{}
+ err := shared.DeepCopy(c, conf)
+ if err != nil {
+ return fmt.Errorf("Unable to copy the configuration: %v", err)
+ }
+
+ // Remove the static remotes
+ for k := range StaticRemotes {
+ delete(conf.Remotes, k)
+ }
+
+ // Create the config file (or truncate an existing one)
+ f, err := os.Create(path)
+ if err != nil {
+ return fmt.Errorf("Unable to create the configuration file: %v", err)
+ }
+ defer f.Close()
+
+ // Write the new config
+ data, err := yaml.Marshal(c)
+ _, err = f.Write(data)
+ if err != nil {
+ return fmt.Errorf("Unable to write the configuration: %v", err)
+ }
+
+ return nil
+}
diff --git a/lxc/config/remote.go b/lxc/config/remote.go
new file mode 100644
index 0000000..dddea37
--- /dev/null
+++ b/lxc/config/remote.go
@@ -0,0 +1,139 @@
+package config
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/lxc/lxd/client"
+ "github.com/lxc/lxd/shared"
+)
+
+// Remote holds details for communication with a remote daemon
+type Remote struct {
+ Addr string `yaml:"addr"`
+ Public bool `yaml:"public"`
+ Protocol string `yaml:"protocol,omitempty"`
+ Static bool `yaml:"-"`
+}
+
+// ParseRemote splits remote and object
+func (c *Config) ParseRemote(raw string) (string, string, error) {
+ result := strings.SplitN(raw, ":", 2)
+ if len(result) == 1 {
+ return c.DefaultRemote, result[0], nil
+ }
+
+ _, ok := c.Remotes[result[0]]
+ if !ok {
+ return "", "", fmt.Errorf("The remote \"%s\" doesn't exist", result[0])
+ }
+
+ return result[0], result[1], nil
+}
+
+// GetContainerServer returns a ContainerServer struct for the remote
+func (c *Config) GetContainerServer(name string) (lxd.ContainerServer, error) {
+ // Get the remote
+ remote, ok := c.Remotes[name]
+ if !ok {
+ return nil, fmt.Errorf("The remote \"%s\" doesn't exist", name)
+ }
+
+ // Sanity checks
+ if remote.Public || remote.Protocol == "simplestreams" {
+ return nil, fmt.Errorf("The remote isn't a private LXD server")
+ }
+
+ // Get connection arguments
+ args := c.getConnectionArgs(name)
+
+ // Unix socket
+ if strings.HasPrefix(remote.Addr, "unix:") {
+ d, err := lxd.ConnectLXDUnix(strings.TrimPrefix(strings.TrimPrefix(remote.Addr, "unix:"), "//"), &args)
+ if err != nil {
+ return nil, err
+ }
+
+ return d, nil
+ }
+
+ // HTTPs
+ if args.TLSClientCert == "" || args.TLSClientKey == "" {
+ return nil, fmt.Errorf("Missing TLS client certificate and key")
+ }
+
+ d, err := lxd.ConnectLXD(remote.Addr, &args)
+ if err != nil {
+ return nil, err
+ }
+
+ return d, nil
+}
+
+// GetImageServer returns a ImageServer struct for the remote
+func (c *Config) GetImageServer(name string) (lxd.ImageServer, error) {
+ // Get the remote
+ remote, ok := c.Remotes[name]
+ if !ok {
+ return nil, fmt.Errorf("The remote \"%s\" doesn't exist", name)
+ }
+
+ // Get connection arguments
+ args := c.getConnectionArgs(name)
+
+ // Unix socket
+ if strings.HasPrefix(remote.Addr, "unix:") {
+ d, err := lxd.ConnectLXDUnix(strings.TrimPrefix(strings.TrimPrefix(remote.Addr, "unix:"), "//"), &args)
+ if err != nil {
+ return nil, err
+ }
+
+ return d, nil
+ }
+
+ // HTTPs (simplestreams)
+ if remote.Protocol == "simplestreams" {
+ d, err := lxd.ConnectSimpleStreams(remote.Addr, &args)
+ if err != nil {
+ return nil, err
+ }
+
+ return d, nil
+ }
+
+ // HTTPs (LXD)
+ d, err := lxd.ConnectPublicLXD(remote.Addr, &args)
+ if err != nil {
+ return nil, err
+ }
+
+ return d, nil
+}
+
+func (c *Config) getConnectionArgs(name string) lxd.ConnectionArgs {
+ args := lxd.ConnectionArgs{
+ UserAgent: c.UserAgent,
+ }
+
+ // Client certificate
+ if !shared.PathExists(c.ConfigPath("client.crt")) {
+ args.TLSClientCert = c.ConfigPath("client.crt")
+ }
+
+ // Client key
+ if !shared.PathExists(c.ConfigPath("client.key")) {
+ args.TLSClientKey = c.ConfigPath("client.key")
+ }
+
+ // Client CA
+ if shared.PathExists(c.ConfigPath("client.ca")) {
+ args.TLSCA = c.ConfigPath("client.ca")
+ }
+
+ // Server certificate
+ if shared.PathExists(c.ServerCertPath(name)) {
+ args.TLSServerCert = c.ServerCertPath(name)
+ }
+
+ return args
+}
From 22aac8a528a16bcf43ec811bf9f8b6883c182fec Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Thu, 30 Mar 2017 17:22:15 -0400
Subject: [PATCH 03/15] doc: Update README.md for new API 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>
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 71cc926..003f605 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ To easily see what LXD is about, you can [try it online](https://linuxcontainers
## Status
-* GoDoc: [![GoDoc](https://godoc.org/github.com/lxc/lxd?status.svg)](https://godoc.org/github.com/lxc/lxd)
+* GoDoc: [![GoDoc](https://godoc.org/github.com/lxc/lxd/client?status.svg)](https://godoc.org/github.com/lxc/lxd/client)
* Jenkins (Linux): [![Build Status](https://jenkins.linuxcontainers.org/job/lxd-github-commit/badge/icon)](https://jenkins.linuxcontainers.org/job/lxd-github-commit/)
* Travis (macOS): [![Build Status](https://travis-ci.org/lxc/lxd.svg?branch=master)](https://travis-ci.org/lxc/lxd/)
* AppVeyor (Windows): [![Build Status](https://ci.appveyor.com/api/projects/status/rb4141dsi2xm3n0x/branch/master?svg=true)](https://ci.appveyor.com/project/lxc/lxd/)
From e27c6bba385683b8e37e905083dc962311b6c761 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Thu, 30 Mar 2017 17:28:50 -0400
Subject: [PATCH 04/15] ci: Update for new 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>
---
.appveyor.yml | 3 ++-
.travis.yml | 3 ++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/.appveyor.yml b/.appveyor.yml
index d9cbb6c..ddc6fcf 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -19,8 +19,9 @@ build_script:
test_script:
- cmd: |-
go test -v ./
- go test -v ./shared
+ go test -v ./client
go test -v ./lxc
+ go test -v ./shared
after_test:
# powershell capture command output into environment variable
diff --git a/.travis.yml b/.travis.yml
index 73e98ad..781302b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -20,8 +20,9 @@ install:
script:
- "make client"
- "go test ./"
- - "go test ./shared"
+ - "go test ./client"
- "go test ./lxc"
+ - "go test ./shared"
notifications:
webhooks: https://linuxcontainers.org/webhook-lxcbot/
From fd5ad1c37123034cf86faef6f214452fb55b7c8f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Fri, 31 Mar 2017 15:21:36 -0400
Subject: [PATCH 05/15] tests: Run golint on client/ and lxc/config/
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>
---
test/suites/static_analysis.sh | 2 ++
1 file changed, 2 insertions(+)
diff --git a/test/suites/static_analysis.sh b/test/suites/static_analysis.sh
index fa8ed26..fb49c22 100644
--- a/test/suites/static_analysis.sh
+++ b/test/suites/static_analysis.sh
@@ -36,6 +36,8 @@ test_static_analysis() {
## golint
if which golint >/dev/null 2>&1; then
+ golint -set_exit_status client/
+ golint -set_exit_status lxc/config/
golint -set_exit_status shared/api/
fi
From fd7c9a485964349e9d262a1924c99297460fc452 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 7 Mar 2017 00:21:07 -0500
Subject: [PATCH 06/15] Port main_activateifneded to new 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>
---
lxd/main_activateifneeded.go | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/lxd/main_activateifneeded.go b/lxd/main_activateifneeded.go
index 7f831f8..da6de2b 100644
--- a/lxd/main_activateifneeded.go
+++ b/lxd/main_activateifneeded.go
@@ -4,7 +4,7 @@ import (
"fmt"
"os"
- "github.com/lxc/lxd"
+ "github.com/lxc/lxd/client"
"github.com/lxc/lxd/shared"
)
@@ -39,7 +39,7 @@ func cmdActivateIfNeeded() error {
value := daemonConfig["core.https_address"].Get()
if value != "" {
shared.LogDebugf("Daemon has core.https_address set, activating...")
- _, err := lxd.NewClient(&lxd.DefaultConfig, "local")
+ _, err := lxd.ConnectLXDUnix("", nil)
return err
}
@@ -67,13 +67,13 @@ func cmdActivateIfNeeded() error {
if c.IsRunning() {
shared.LogDebugf("Daemon has running containers, activating...")
- _, err := lxd.NewClient(&lxd.DefaultConfig, "local")
+ _, err := lxd.ConnectLXDUnix("", nil)
return err
}
if lastState == "RUNNING" || lastState == "Running" || shared.IsTrue(autoStart) {
shared.LogDebugf("Daemon has auto-started containers, activating...")
- _, err := lxd.NewClient(&lxd.DefaultConfig, "local")
+ _, err := lxd.ConnectLXDUnix("", nil)
return err
}
}
From 80b31036dda574db276b104f9f7875de0694effe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 7 Mar 2017 02:23:23 -0500
Subject: [PATCH 07/15] Port main_callhook to new 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>
---
lxd/main_callhook.go | 32 +++++++++-----------------------
1 file changed, 9 insertions(+), 23 deletions(-)
diff --git a/lxd/main_callhook.go b/lxd/main_callhook.go
index 31e2231..c8507fd 100644
--- a/lxd/main_callhook.go
+++ b/lxd/main_callhook.go
@@ -2,15 +2,14 @@ package main
import (
"fmt"
- "net/http"
"os"
"time"
- "github.com/lxc/lxd"
- "github.com/lxc/lxd/shared/api"
+ "github.com/lxc/lxd/client"
)
func cmdCallHook(args []string) error {
+ // Parse the arguments
if len(args) < 4 {
return fmt.Errorf("Invalid arguments")
}
@@ -20,18 +19,14 @@ func cmdCallHook(args []string) error {
state := args[3]
target := ""
- err := os.Setenv("LXD_DIR", path)
+ // Connect to LXD
+ c, err := lxd.ConnectLXDUnix(fmt.Sprintf("%s/unix.socket", path), nil)
if err != nil {
return err
}
- c, err := lxd.NewClient(&lxd.DefaultConfig, "local")
- if err != nil {
- return err
- }
-
- url := fmt.Sprintf("%s/internal/containers/%s/on%s", c.BaseURL, id, state)
-
+ // Prepare the request URL
+ url := fmt.Sprintf("/internal/containers/%s/on%s", id, state)
if state == "stop" {
target = os.Getenv("LXC_TARGET")
if target == "" {
@@ -40,20 +35,10 @@ func cmdCallHook(args []string) error {
url = fmt.Sprintf("%s?target=%s", url, target)
}
- req, err := http.NewRequest("GET", url, nil)
- if err != nil {
- return err
- }
-
+ // Setup the request
hook := make(chan error, 1)
go func() {
- raw, err := c.Http.Do(req)
- if err != nil {
- hook <- err
- return
- }
-
- _, err = lxd.HoistResponse(raw, api.SyncResponse)
+ _, _, err := c.RawQuery("GET", url, nil, "")
if err != nil {
hook <- err
return
@@ -62,6 +47,7 @@ func cmdCallHook(args []string) error {
hook <- nil
}()
+ // Handle the timeout
select {
case err := <-hook:
if err != nil {
From 949505db96df754db022efda5eda6f3f9e755995 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 7 Mar 2017 02:23:45 -0500
Subject: [PATCH 08/15] Port main_import to new 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>
---
lxd/main_import.go | 33 ++++-----------------------------
1 file changed, 4 insertions(+), 29 deletions(-)
diff --git a/lxd/main_import.go b/lxd/main_import.go
index 6f3a600..e7b141f 100644
--- a/lxd/main_import.go
+++ b/lxd/main_import.go
@@ -1,47 +1,22 @@
package main
import (
- "bytes"
- "encoding/json"
- "fmt"
- "net/http"
-
- "github.com/lxc/lxd"
- "github.com/lxc/lxd/shared"
- "github.com/lxc/lxd/shared/api"
- "github.com/lxc/lxd/shared/version"
+ "github.com/lxc/lxd/client"
)
func cmdImport(args []string) error {
name := args[1]
- b := shared.Jmap{
+ req := map[string]interface{}{
"name": name,
"force": *argForce,
}
- buf := bytes.Buffer{}
- err := json.NewEncoder(&buf).Encode(b)
- if err != nil {
- return err
- }
-
- c, err := lxd.NewClient(&lxd.DefaultConfig, "local")
+ c, err := lxd.ConnectLXDUnix("", nil)
if err != nil {
return err
}
- url := fmt.Sprintf("%s/internal/containers", c.BaseURL)
-
- req, err := http.NewRequest("POST", url, &buf)
- if err != nil {
- return err
- }
-
- req.Header.Set("User-Agent", version.UserAgent)
- req.Header.Set("Content-Type", "application/json")
-
- raw, err := c.Http.Do(req)
- _, err = lxd.HoistResponse(raw, api.SyncResponse)
+ _, _, err = c.RawQuery("POST", "/internal/containers", req, "")
if err != nil {
return err
}
From 7c8b09989faf7c4a512d3d1bcf07726c5edcd035 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 7 Mar 2017 02:23:57 -0500
Subject: [PATCH 09/15] Port main_ready to new 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>
---
lxd/main_ready.go | 19 +++----------------
1 file changed, 3 insertions(+), 16 deletions(-)
diff --git a/lxd/main_ready.go b/lxd/main_ready.go
index 1093234..fb4f0df 100644
--- a/lxd/main_ready.go
+++ b/lxd/main_ready.go
@@ -1,29 +1,16 @@
package main
import (
- "net/http"
-
- "github.com/lxc/lxd"
- "github.com/lxc/lxd/shared/api"
+ "github.com/lxc/lxd/client"
)
func cmdReady() error {
- c, err := lxd.NewClient(&lxd.DefaultConfig, "local")
- if err != nil {
- return err
- }
-
- req, err := http.NewRequest("PUT", c.BaseURL+"/internal/ready", nil)
- if err != nil {
- return err
- }
-
- raw, err := c.Http.Do(req)
+ c, err := lxd.ConnectLXDUnix("", nil)
if err != nil {
return err
}
- _, err = lxd.HoistResponse(raw, api.SyncResponse)
+ _, _, err = c.RawQuery("PUT", "/internal/ready", nil, "")
if err != nil {
return err
}
From 36002eabc034b23b46bfcfc8fca29ca3adecc81f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 7 Mar 2017 02:24:09 -0500
Subject: [PATCH 10/15] Port main_waitready to new 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>
---
lxd/main_waitready.go | 20 +++-----------------
1 file changed, 3 insertions(+), 17 deletions(-)
diff --git a/lxd/main_waitready.go b/lxd/main_waitready.go
index 74534b3..057abe7 100644
--- a/lxd/main_waitready.go
+++ b/lxd/main_waitready.go
@@ -2,11 +2,9 @@ package main
import (
"fmt"
- "net/http"
"time"
- "github.com/lxc/lxd"
- "github.com/lxc/lxd/shared/api"
+ "github.com/lxc/lxd/client"
)
func cmdWaitReady() error {
@@ -21,25 +19,13 @@ func cmdWaitReady() error {
finger := make(chan error, 1)
go func() {
for {
- c, err := lxd.NewClient(&lxd.DefaultConfig, "local")
+ c, err := lxd.ConnectLXDUnix("", nil)
if err != nil {
time.Sleep(500 * time.Millisecond)
continue
}
- req, err := http.NewRequest("GET", c.BaseURL+"/internal/ready", nil)
- if err != nil {
- time.Sleep(500 * time.Millisecond)
- continue
- }
-
- raw, err := c.Http.Do(req)
- if err != nil {
- time.Sleep(500 * time.Millisecond)
- continue
- }
-
- _, err = lxd.HoistResponse(raw, api.SyncResponse)
+ _, _, err = c.RawQuery("GET", "/internal/ready", nil, "")
if err != nil {
time.Sleep(500 * time.Millisecond)
continue
From 56d59cbba02573a898b1d4c0363de7685eeceb62 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 8 Mar 2017 22:57:31 -0500
Subject: [PATCH 11/15] Port main_daemon to new 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>
---
lxd/containers_post.go | 17 +-
lxd/daemon.go | 69 +------
lxd/daemon_images.go | 487 +++++++++++++++++++++----------------------------
lxd/images.go | 21 ++-
lxd/migrate.go | 8 +-
lxd/remote.go | 26 ---
6 files changed, 234 insertions(+), 394 deletions(-)
delete mode 100644 lxd/remote.go
diff --git a/lxd/containers_post.go b/lxd/containers_post.go
index afa955f..34e8ae2 100644
--- a/lxd/containers_post.go
+++ b/lxd/containers_post.go
@@ -92,26 +92,27 @@ func createFromImage(d *Daemon, req *api.ContainersPost) Response {
Profiles: req.Profiles,
}
+ var info *api.Image
if req.Source.Server != "" {
- hash, err = d.ImageDownload(
+ info, err = d.ImageDownload(
op, req.Source.Server, req.Source.Protocol, req.Source.Certificate, req.Source.Secret,
hash, true, daemonConfig["images.auto_update_cached"].GetBool(), "")
if err != nil {
return err
}
+ } else {
+ _, info, err = dbImageGet(d.db, hash, false, false)
+ if err != nil {
+ return err
+ }
}
- _, imgInfo, err := dbImageGet(d.db, hash, false, false)
- if err != nil {
- return err
- }
-
- args.Architecture, err = osarch.ArchitectureId(imgInfo.Architecture)
+ args.Architecture, err = osarch.ArchitectureId(info.Architecture)
if err != nil {
return err
}
- _, err = containerCreateFromImage(d, args, imgInfo.Fingerprint)
+ _, err = containerCreateFromImage(d, args, info.Fingerprint)
return err
}
diff --git a/lxd/daemon.go b/lxd/daemon.go
index 6ebe8ed..f313ea3 100644
--- a/lxd/daemon.go
+++ b/lxd/daemon.go
@@ -29,9 +29,8 @@ import (
"github.com/syndtr/gocapability/capability"
"gopkg.in/tomb.v2"
- "github.com/lxc/lxd"
+ "github.com/lxc/lxd/client"
"github.com/lxc/lxd/shared"
- "github.com/lxc/lxd/shared/api"
"github.com/lxc/lxd/shared/logging"
"github.com/lxc/lxd/shared/osarch"
"github.com/lxc/lxd/shared/version"
@@ -148,70 +147,6 @@ func (d *Daemon) httpClient(certificate string) (*http.Client, error) {
return &myhttp, nil
}
-func (d *Daemon) httpGetSync(url string, certificate string) (*api.Response, error) {
- var err error
-
- req, err := http.NewRequest("GET", url, nil)
- if err != nil {
- return nil, err
- }
-
- req.Header.Set("User-Agent", version.UserAgent)
-
- myhttp, err := d.httpClient(certificate)
- if err != nil {
- return nil, err
- }
-
- r, err := myhttp.Do(req)
- if err != nil {
- return nil, err
- }
-
- resp, err := lxd.ParseResponse(r)
- if err != nil {
- return nil, err
- }
-
- if resp.Type != api.SyncResponse {
- return nil, fmt.Errorf("unexpected non-sync response")
- }
-
- return resp, nil
-}
-
-func (d *Daemon) httpGetFile(url string, certificate string) (*http.Response, error) {
- var err error
-
- myhttp, err := d.httpClient(certificate)
- if err != nil {
- return nil, err
- }
-
- req, err := http.NewRequest("GET", url, nil)
- if err != nil {
- return nil, err
- }
-
- req.Header.Set("User-Agent", version.UserAgent)
-
- raw, err := myhttp.Do(req)
- if err != nil {
- return nil, err
- }
-
- if raw.StatusCode != 200 {
- _, err := lxd.HoistResponse(raw, api.ErrorResponse)
- if err != nil {
- return nil, err
- }
-
- return nil, fmt.Errorf("non-200 status with no error response?")
- }
-
- return raw, nil
-}
-
func readMyCert() (string, string, error) {
certf := shared.VarPath("server.crt")
keyf := shared.VarPath("server.key")
@@ -1031,7 +966,7 @@ func (d *Daemon) Init() error {
// If the socket exists, let's try to connect to it and see if there's
// a lxd running.
if shared.PathExists(localSocketPath) {
- _, err := lxd.NewClient(&lxd.DefaultConfig, "local")
+ _, err := lxd.ConnectLXDUnix("", nil)
if err != nil {
shared.LogDebugf("Detected stale unix socket, deleting")
// Connecting failed, so let's delete the socket and
diff --git a/lxd/daemon_images.go b/lxd/daemon_images.go
index f55559e..b43d4d5 100644
--- a/lxd/daemon_images.go
+++ b/lxd/daemon_images.go
@@ -1,11 +1,11 @@
package main
import (
+ "crypto/sha256"
"fmt"
"io"
"io/ioutil"
- "mime"
- "mime/multipart"
+ "net/http"
"os"
"path/filepath"
"strings"
@@ -14,10 +14,10 @@ import (
"gopkg.in/yaml.v2"
+ "github.com/lxc/lxd/client"
"github.com/lxc/lxd/shared"
"github.com/lxc/lxd/shared/api"
"github.com/lxc/lxd/shared/ioprogress"
- "github.com/lxc/lxd/shared/simplestreams"
"github.com/lxc/lxd/shared/version"
log "gopkg.in/inconshreveable/log15.v2"
@@ -28,8 +28,9 @@ type imageStreamCacheEntry struct {
Aliases []api.ImageAliasesEntry `yaml:"aliases"`
Certificate string `yaml:"certificate"`
Fingerprints []string `yaml:"fingerprints"`
- expiry time.Time
- ss *simplestreams.SimpleStreams
+
+ expiry time.Time
+ remote lxd.ImageServer
}
var imageStreamCache = map[string]*imageStreamCacheEntry{}
@@ -71,55 +72,64 @@ func imageLoadStreamCache(d *Daemon) error {
}
for url, entry := range imageStreamCache {
- if entry.ss == nil {
- myhttp, err := d.httpClient(entry.Certificate)
+ if entry.remote == nil {
+ remote, err := lxd.ConnectSimpleStreams(url, &lxd.ConnectionArgs{
+ TLSServerCert: entry.Certificate,
+ UserAgent: version.UserAgent,
+ Proxy: d.proxy,
+ })
if err != nil {
- return err
+ continue
}
- ss := simplestreams.NewClient(url, *myhttp, version.UserAgent)
- entry.ss = ss
+ entry.remote = remote
}
}
return nil
}
-// 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, alias string, forContainer bool, autoUpdate bool, storagePool string) (string, error) {
+// ImageDownload resolves the image fingerprint and if not in the database, downloads it
+func (d *Daemon) ImageDownload(op *operation, server string, protocol string, certificate string, secret string, alias string, forContainer bool, autoUpdate bool, storagePool string) (*api.Image, error) {
var err error
- var ss *simplestreams.SimpleStreams
var ctxMap log.Ctx
+ var remote lxd.ImageServer
+ var info *api.Image
+
+ // Default protocol is LXD
if protocol == "" {
protocol = "lxd"
}
+ // Default the fingerprint to the alias string we received
fp := alias
- // Expand aliases
+ // Attempt to resolve the alias
if protocol == "simplestreams" {
imageStreamCacheLock.Lock()
entry, _ := imageStreamCache[server]
if entry == nil || entry.expiry.Before(time.Now()) {
+ // Add a new entry to the cache
refresh := func() (*imageStreamCacheEntry, error) {
// Setup simplestreams client
- myhttp, err := d.httpClient(certificate)
+ remote, err = lxd.ConnectSimpleStreams(server, &lxd.ConnectionArgs{
+ TLSServerCert: certificate,
+ UserAgent: version.UserAgent,
+ Proxy: d.proxy,
+ })
if err != nil {
return nil, err
}
- ss = simplestreams.NewClient(server, *myhttp, version.UserAgent)
-
// Get all aliases
- aliases, err := ss.ListAliases()
+ aliases, err := remote.GetImageAliases()
if err != nil {
return nil, err
}
// Get all fingerprints
- images, err := ss.ListImages()
+ images, err := remote.GetImages()
if err != nil {
return nil, err
}
@@ -130,7 +140,7 @@ func (d *Daemon) ImageDownload(op *operation, server string, protocol string, ce
}
// Generate cache entry
- entry = &imageStreamCacheEntry{ss: ss, Aliases: aliases, Certificate: certificate, Fingerprints: fingerprints, expiry: time.Now().Add(time.Hour)}
+ entry = &imageStreamCacheEntry{remote: remote, Aliases: aliases, Certificate: certificate, Fingerprints: fingerprints, expiry: time.Now().Add(time.Hour)}
imageStreamCache[server] = entry
imageSaveStreamCache()
@@ -148,90 +158,112 @@ func (d *Daemon) ImageDownload(op *operation, server string, protocol string, ce
} else {
// Failed to fetch entry and nothing in cache
imageStreamCacheLock.Unlock()
- return "", err
+ return nil, err
}
} else {
+ // use the existing entry
shared.LogDebug("Using SimpleStreams cache entry", log.Ctx{"server": server, "expiry": entry.expiry})
- ss = entry.ss
+ remote = entry.remote
}
imageStreamCacheLock.Unlock()
- // Expand aliases
- for _, alias := range entry.Aliases {
- if alias.Name != fp {
+ // Look for a matching alias
+ for _, entry := range entry.Aliases {
+ if entry.Name != fp {
continue
}
- fp = alias.Target
+ fp = entry.Target
break
}
- // Expand fingerprint
- for _, fingerprint := range entry.Fingerprints {
- if !strings.HasPrefix(fingerprint, fp) {
- continue
+ // Expand partial fingerprints
+ matches := []string{}
+ for _, entry := range entry.Fingerprints {
+ if strings.HasPrefix(entry, fp) {
+ matches = append(matches, entry)
}
+ }
- if fp == alias {
- alias = fingerprint
- }
- fp = fingerprint
- break
+ if len(matches) == 1 {
+ fp = matches[0]
+ } else if len(matches) > 1 {
+ return nil, fmt.Errorf("Provided partial image fingerprint matches more than one image")
}
} else if protocol == "lxd" {
- target, err := remoteGetImageFingerprint(d, server, certificate, fp)
- if err == nil && target != "" {
- fp = target
+ // Setup LXD client
+ remote, err = lxd.ConnectPublicLXD(server, &lxd.ConnectionArgs{
+ TLSServerCert: certificate,
+ UserAgent: version.UserAgent,
+ Proxy: d.proxy,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // For public images, handle aliases and initial metadata
+ if secret == "" {
+ // Look for a matching alias
+ entry, _, err := remote.GetImageAlias(fp)
+ if err == nil {
+ fp = entry.Target
+ }
+
+ // Expand partial fingerprints
+ info, _, err = remote.GetImage(fp)
+ if err != nil {
+ return nil, err
+ }
+
+ fp = info.Fingerprint
}
}
- // Check if the image already exists on any storage pool.
- _, imgInfo, err := dbImageGet(d.db, fp, false, false)
+ // Check if the image already exists (partial hash match)
+ _, imgInfo, err := dbImageGet(d.db, fp, false, true)
if err == nil {
shared.LogDebug("Image already exists in the db", log.Ctx{"image": fp})
+ info = imgInfo
+ // If not requested in a particular pool, we're done.
if storagePool == "" {
- return fp, nil
+ return info, nil
}
- // Get the ID of the storage pool on which a storage volume for
- // the image needs to exist.
+ // Get the ID of the target storage pool
poolID, err := dbStoragePoolGetID(d.db, storagePool)
if err != nil {
- return "", err
+ return nil, err
}
- // Get the IDs of all storage pools on which a storage volume
- // for the requested image currently exists.
- poolIDs, err := dbImageGetPools(d.db, imgInfo.Fingerprint)
+ // Check if the image is already in the pool
+ poolIDs, err := dbImageGetPools(d.db, info.Fingerprint)
if err != nil {
- return "", err
+ return nil, err
}
- // Check if the image already exists on the current storage
- // pool.
if shared.Int64InSlice(poolID, poolIDs) {
shared.LogDebugf("Image already exists on storage pool \"%s\".", storagePool)
- return fp, nil
+ return info, nil
}
+ // Import the image in the pool
shared.LogDebugf("Image does not exist on storage pool \"%s\".", storagePool)
- // Create a duplicate entry for the image.
- err = imageCreateInPool(d, imgInfo, storagePool)
+ err = imageCreateInPool(d, info, storagePool)
if err != nil {
shared.LogDebugf("Failed to create image on storage pool \"%s\": %s.", storagePool, err)
- return "", err
+ return nil, err
}
shared.LogDebugf("Created image on storage pool \"%s\".", storagePool)
- return fp, nil
+ return info, nil
}
- // Now check if we already downloading the image
+ // Deal with parallel downloads
imagesDownloadingLock.Lock()
if waitChannel, ok := imagesDownloading[fp]; ok {
- // We already download the image
+ // We are already downloading the image
imagesDownloadingLock.Unlock()
shared.LogDebug(
@@ -239,23 +271,17 @@ func (d *Daemon) ImageDownload(op *operation, server string, protocol string, ce
log.Ctx{"image": fp})
// Wait until the download finishes (channel closes)
- if _, ok := <-waitChannel; ok {
- shared.LogWarnf("Value transmitted over image lock semaphore?")
- }
+ <-waitChannel
- if _, _, err := dbImageGet(d.db, fp, false, true); err != nil {
- shared.LogError(
- "Previous download didn't succeed",
- log.Ctx{"image": fp})
-
- return "", fmt.Errorf("Previous download didn't succeed")
+ // Grab the database entry
+ _, imgInfo, err := dbImageGet(d.db, fp, false, true)
+ if err != nil {
+ // Other download failed, lets try again
+ shared.LogError("Other image download didn't succeed", log.Ctx{"image": fp})
+ } else {
+ // Other download succeeded, we're done
+ return imgInfo, nil
}
-
- shared.LogDebug(
- "Previous download succeeded",
- log.Ctx{"image": fp})
-
- return fp, nil
}
// Add the download to the queue
@@ -278,22 +304,23 @@ func (d *Daemon) ImageDownload(op *operation, server string, protocol string, ce
} else {
ctxMap = log.Ctx{"trigger": op.url, "image": fp, "operation": op.id, "alias": alias, "server": server}
}
-
shared.LogInfo("Downloading image", ctxMap)
- exporturl := server
-
- var info api.Image
- info.Fingerprint = fp
-
+ // Cleanup any leftover from a past attempt
destDir := shared.VarPath("images")
destName := filepath.Join(destDir, fp)
- if shared.PathExists(destName) {
- os.Remove(filepath.Join(destDir, fp))
- os.Remove(filepath.Join(destDir, fp+".root"))
+
+ failure := true
+ cleanup := func() {
+ if failure {
+ os.Remove(destName)
+ os.Remove(destName + ".rootfs")
+ }
}
+ defer cleanup()
- progress := func(progressInt int64, speedInt int64) {
+ // Setup a progress handler
+ progress := func(progress lxd.ProgressData) {
if op == nil {
return
}
@@ -303,278 +330,174 @@ func (d *Daemon) ImageDownload(op *operation, server string, protocol string, ce
meta = make(map[string]interface{})
}
- progress := fmt.Sprintf("%d%% (%s/s)", progressInt, shared.GetByteSizeString(speedInt, 2))
-
- if meta["download_progress"] != progress {
- meta["download_progress"] = progress
+ if meta["download_progress"] != progress.Text {
+ meta["download_progress"] = progress.Text
op.UpdateMetadata(meta)
}
}
- if protocol == "lxd" {
- /* grab the metadata from /1.0/images/%s */
- var url string
- if secret != "" {
- url = fmt.Sprintf(
- "%s/%s/images/%s?secret=%s",
- server, version.APIVersion, fp, secret)
- } else {
- url = fmt.Sprintf("%s/%s/images/%s", server, version.APIVersion, fp)
- }
-
- resp, err := d.httpGetSync(url, certificate)
+ if protocol == "lxd" || protocol == "simplestreams" {
+ // Create the target files
+ dest, err := os.Create(destName)
if err != nil {
- shared.LogError(
- "Failed to download image metadata",
- log.Ctx{"image": fp, "err": err})
-
- return "", err
- }
-
- if err := resp.MetadataAsStruct(&info); err != nil {
- return "", err
+ return nil, err
}
+ defer dest.Close()
- /* now grab the actual file from /1.0/images/%s/export */
- if secret != "" {
- exporturl = fmt.Sprintf(
- "%s/%s/images/%s/export?secret=%s",
- server, version.APIVersion, fp, secret)
-
- } else {
- exporturl = fmt.Sprintf(
- "%s/%s/images/%s/export",
- server, version.APIVersion, fp)
- }
- } else if protocol == "simplestreams" {
- err := ss.Download(fp, "meta", destName, nil)
+ destRootfs, err := os.Create(destName + ".rootfs")
if err != nil {
- return "", err
+ return nil, err
}
+ defer destRootfs.Close()
- err = ss.Download(fp, "root", destName+".rootfs", progress)
- if err != nil {
- return "", err
+ // Get the image information
+ if info == nil {
+ if secret != "" {
+ info, _, err = remote.GetPrivateImage(fp, secret)
+ } else {
+ info, _, err = remote.GetImage(fp)
+ }
+ if err != nil {
+ return nil, err
+ }
}
- info, err := ss.GetImage(fp)
- if err != nil {
- return "", err
+ // Download the image
+ var resp *lxd.ImageFileResponse
+ request := lxd.ImageFileRequest{
+ MetaFile: io.WriteSeeker(dest),
+ RootfsFile: io.WriteSeeker(destRootfs),
+ ProgressHandler: progress,
}
- info.Public = false
- info.AutoUpdate = autoUpdate
-
- if storagePool != "" {
- err = imageCreateInPool(d, info, storagePool)
- if err != nil {
- return "", err
- }
+ if secret != "" {
+ resp, err = remote.GetPrivateImageFile(fp, secret, request)
+ } else {
+ resp, err = remote.GetImageFile(fp, request)
}
-
- // Create the database entry
- err = dbImageInsert(d.db, info.Fingerprint, info.Filename, info.Size, info.Public, info.AutoUpdate, info.Architecture, info.CreatedAt, info.ExpiresAt, info.Properties)
if err != nil {
- return "", err
+ return nil, 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)
+ // Deal with unified images
+ if resp.RootfsSize == 0 {
+ err := os.Remove(destName + ".rootfs")
if err != nil {
- return "", err
+ return nil, err
}
}
-
- shared.LogInfo("Image downloaded", ctxMap)
-
- if forContainer {
- return fp, dbImageLastAccessInit(d.db, fp)
- }
-
- return fp, nil
- }
-
- raw, err := d.httpGetFile(exporturl, certificate)
- if err != nil {
- shared.LogError(
- "Failed to download image",
- log.Ctx{"image": fp, "err": err})
- return "", err
- }
- info.Size = raw.ContentLength
-
- ctype, ctypeParams, err := mime.ParseMediaType(raw.Header.Get("Content-Type"))
- if err != nil {
- ctype = "application/octet-stream"
- }
-
- body := &ioprogress.ProgressReader{
- ReadCloser: raw.Body,
- Tracker: &ioprogress.ProgressTracker{
- Length: raw.ContentLength,
- Handler: progress,
- },
- }
-
- if ctype == "multipart/form-data" {
- // Parse the POST data
- mr := multipart.NewReader(body, ctypeParams["boundary"])
-
- // Get the metadata tarball
- part, err := mr.NextPart()
+ } else if protocol == "direct" {
+ // Setup HTTP client
+ httpClient, err := d.httpClient(certificate)
if err != nil {
- shared.LogError(
- "Invalid multipart image",
- log.Ctx{"image": fp, "err": err})
-
- return "", err
- }
-
- if part.FormName() != "metadata" {
- shared.LogError(
- "Invalid multipart image",
- log.Ctx{"image": fp, "err": err})
-
- return "", fmt.Errorf("Invalid multipart image")
+ return nil, err
}
- destName = filepath.Join(destDir, info.Fingerprint)
- f, err := os.Create(destName)
+ req, err := http.NewRequest("GET", server, nil)
if err != nil {
- shared.LogError(
- "Failed to save image",
- log.Ctx{"image": fp, "err": err})
-
- return "", err
+ return nil, err
}
- _, err = io.Copy(f, part)
- f.Close()
+ req.Header.Set("User-Agent", version.UserAgent)
+ // Make the request
+ raw, err := httpClient.Do(req)
if err != nil {
- shared.LogError(
- "Failed to save image",
- log.Ctx{"image": fp, "err": err})
-
- return "", err
+ return nil, err
}
- // Get the rootfs tarball
- part, err = mr.NextPart()
- if err != nil {
- shared.LogError(
- "Invalid multipart image",
- log.Ctx{"image": fp, "err": err})
-
- return "", err
+ if raw.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("Unable to fetch %s: %s", server, raw.Status)
}
- if part.FormName() != "rootfs" {
- shared.LogError(
- "Invalid multipart image",
- log.Ctx{"image": fp})
- return "", fmt.Errorf("Invalid multipart image")
+ // Progress handler
+ body := &ioprogress.ProgressReader{
+ ReadCloser: raw.Body,
+ Tracker: &ioprogress.ProgressTracker{
+ Length: raw.ContentLength,
+ Handler: func(percent int64, speed int64) {
+ progress(lxd.ProgressData{Text: fmt.Sprintf("%d%% (%s/s)", percent, shared.GetByteSizeString(speed, 2))})
+ },
+ },
}
- destName = filepath.Join(destDir, info.Fingerprint+".rootfs")
- f, err = os.Create(destName)
+ // Create the target files
+ f, err := os.Create(destName)
if err != nil {
- shared.LogError(
- "Failed to save image",
- log.Ctx{"image": fp, "err": err})
- return "", err
+ return nil, err
}
+ defer f.Close()
- _, err = io.Copy(f, part)
- f.Close()
-
- if err != nil {
- shared.LogError(
- "Failed to save image",
- log.Ctx{"image": fp, "err": err})
- return "", err
- }
- } else {
- destName = filepath.Join(destDir, info.Fingerprint)
+ // Hashing
+ sha256 := sha256.New()
- f, err := os.Create(destName)
+ // Download the image
+ size, err := io.Copy(io.MultiWriter(f, sha256), body)
if err != nil {
- shared.LogError(
- "Failed to save image",
- log.Ctx{"image": fp, "err": err})
-
- return "", err
+ return nil, err
}
- _, err = io.Copy(f, body)
- f.Close()
-
- if err != nil {
- shared.LogError(
- "Failed to save image",
- log.Ctx{"image": fp, "err": err})
- return "", err
+ // Validate hash
+ result := fmt.Sprintf("%x", sha256.Sum(nil))
+ if result != fp {
+ return nil, fmt.Errorf("Hash mismatch for %s: %s != %s", server, result, fp)
}
- }
- if protocol == "direct" {
+ // Parse the image
imageMeta, err := getImageMetadata(destName)
if err != nil {
- return "", err
+ return nil, err
}
+ info.Size = size
info.Architecture = imageMeta.Architecture
info.CreatedAt = time.Unix(imageMeta.CreationDate, 0)
info.ExpiresAt = time.Unix(imageMeta.ExpiryDate, 0)
info.Properties = imageMeta.Properties
}
- // By default, make all downloaded images private
+ // Override visiblity
info.Public = false
- if alias != fp && secret == "" {
- info.AutoUpdate = autoUpdate
- }
-
- if storagePool != "" {
- err = imageCreateInPool(d, &info, storagePool)
- if err != nil {
- return "", err
- }
- }
-
// Create the database entry
err = dbImageInsert(d.db, info.Fingerprint, info.Filename, info.Size, info.Public, info.AutoUpdate, info.Architecture, info.CreatedAt, info.ExpiresAt, info.Properties)
if err != nil {
- shared.LogError(
- "Failed to create image",
- log.Ctx{"image": fp, "err": err})
-
- return "", err
+ return nil, fmt.Errorf("here: %v: %s", err, info.Fingerprint)
}
+ // Image is in the DB now, don't wipe on-disk files on failure
+ failure = false
+
if alias != fp {
id, _, err := dbImageGet(d.db, fp, false, true)
if err != nil {
- return "", err
+ return nil, err
}
err = dbImageSourceInsert(d.db, id, server, protocol, "", alias)
if err != nil {
- return "", err
+ return nil, err
}
+
+ info.AutoUpdate = autoUpdate
}
- shared.LogInfo("Image downloaded", ctxMap)
+ // Import into the requested storage pool
+ if storagePool != "" {
+ err = imageCreateInPool(d, info, storagePool)
+ if err != nil {
+ return nil, err
+ }
+ }
+ // Mark the image as "cached" if downloading for a container
if forContainer {
- return fp, dbImageLastAccessInit(d.db, fp)
+ err := dbImageLastAccessInit(d.db, fp)
+ if err != nil {
+ return nil, err
+ }
}
- return fp, nil
+ shared.LogInfo("Image downloaded", ctxMap)
+ return info, nil
}
diff --git a/lxd/images.go b/lxd/images.go
index 98d7782..7bbcba9 100644
--- a/lxd/images.go
+++ b/lxd/images.go
@@ -338,12 +338,12 @@ func imgPostRemoteInfo(d *Daemon, req api.ImagesPost, op *operation) (*api.Image
return nil, 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, req.AutoUpdate, "")
+ info, err := d.ImageDownload(op, req.Source.Server, req.Source.Protocol, req.Source.Certificate, req.Source.Secret, hash, false, req.AutoUpdate, "")
if err != nil {
return nil, err
}
- id, info, err := dbImageGet(d.db, hash, false, false)
+ id, info, err := dbImageGet(d.db, info.Fingerprint, false, true)
if err != nil {
return nil, err
}
@@ -407,12 +407,12 @@ func imgPostURLInfo(d *Daemon, req api.ImagesPost, op *operation) (*api.Image, e
}
// Import the image
- hash, err = d.ImageDownload(op, url, "direct", "", "", hash, false, req.AutoUpdate, "")
+ info, err := d.ImageDownload(op, url, "direct", "", "", hash, false, req.AutoUpdate, "")
if err != nil {
return nil, err
}
- id, info, err := dbImageGet(d.db, hash, false, false)
+ id, info, err := dbImageGet(d.db, info.Fingerprint, false, false)
if err != nil {
return nil, err
}
@@ -908,15 +908,18 @@ func autoUpdateImages(d *Daemon) {
shared.LogDebug("Processing image", log.Ctx{"fp": fp, "server": source.Server, "protocol": source.Protocol, "alias": source.Alias})
// Update the image on each pool where it currently exists.
- var hash string
+ hash := fp
for _, poolName := range poolNames {
- hash, err = d.ImageDownload(nil, source.Server, source.Protocol, "", "", source.Alias, false, true, poolName)
+ newInfo, err := d.ImageDownload(nil, source.Server, source.Protocol, "", "", source.Alias, false, true, poolName)
+ if err != nil {
+ shared.LogError("Failed to update the image", log.Ctx{"err": err, "fp": fp})
+ continue
+ }
+
+ hash = newInfo.Fingerprint
if hash == fp {
shared.LogDebug("Already up to date", log.Ctx{"fp": fp})
continue
- } else if err != nil {
- shared.LogError("Failed to update the image", log.Ctx{"err": err, "fp": fp})
- continue
}
newId, _, err := dbImageGet(d.db, hash, false, true)
diff --git a/lxd/migrate.go b/lxd/migrate.go
index 5610517..1e1e43b 100644
--- a/lxd/migrate.go
+++ b/lxd/migrate.go
@@ -20,7 +20,6 @@ import (
"github.com/gorilla/websocket"
"gopkg.in/lxc/go-lxc.v2"
- "github.com/lxc/lxd"
"github.com/lxc/lxd/shared"
)
@@ -638,7 +637,12 @@ func (c *migrationSink) connectWithSecret(secret string) (*websocket.Conn, error
// The URL is a https URL to the operation, mangle to be a wss URL to the secret
wsUrl := fmt.Sprintf("wss://%s/websocket?%s", strings.TrimPrefix(c.url, "https://"), query.Encode())
- return lxd.WebsocketDial(c.dialer, wsUrl)
+ conn, _, err := c.dialer.Dial(wsUrl, http.Header{})
+ if err != nil {
+ return nil, err
+ }
+
+ return conn, err
}
func (s *migrationSink) Metadata() interface{} {
diff --git a/lxd/remote.go b/lxd/remote.go
deleted file mode 100644
index da8bfc4..0000000
--- a/lxd/remote.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package main
-
-import (
- "fmt"
-
- "github.com/lxc/lxd/shared/api"
- "github.com/lxc/lxd/shared/version"
-)
-
-func remoteGetImageFingerprint(d *Daemon, server string, certificate string, alias string) (string, error) {
- url := fmt.Sprintf(
- "%s/%s/images/aliases/%s",
- server, version.APIVersion, alias)
-
- resp, err := d.httpGetSync(url, certificate)
- if err != nil {
- return "", err
- }
-
- var result api.ImageAliasesEntry
- if err = resp.MetadataAsStruct(&result); err != nil {
- return "", fmt.Errorf("Error reading alias")
- }
-
- return result.Target, nil
-}
From 962e494029867146efa5bbbb6dbaae89bd279734 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Fri, 17 Mar 2017 16:40:57 +0100
Subject: [PATCH 12/15] Port main_init to new 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>
---
lxd/main_init.go | 136 +++++++++++++++++++++++++++++++++++++++++++++----------
1 file changed, 112 insertions(+), 24 deletions(-)
diff --git a/lxd/main_init.go b/lxd/main_init.go
index e86d210..a51e893 100644
--- a/lxd/main_init.go
+++ b/lxd/main_init.go
@@ -11,8 +11,9 @@ import (
"golang.org/x/crypto/ssh/terminal"
- "github.com/lxc/lxd"
+ "github.com/lxc/lxd/client"
"github.com/lxc/lxd/shared"
+ "github.com/lxc/lxd/shared/api"
)
func cmdInit() error {
@@ -160,13 +161,78 @@ func cmdInit() error {
}
}
- // Confirm that LXD is online
- c, err := lxd.NewClient(&lxd.DefaultConfig, "local")
+ // Connect to LXD
+ c, err := lxd.ConnectLXDUnix("", nil)
if err != nil {
return fmt.Errorf("Unable to talk to LXD: %s", err)
}
- pools, err := c.ListStoragePools()
+ setServerConfig := func(key string, value string) error {
+ server, etag, err := c.GetServer()
+ if err != nil {
+ return err
+ }
+
+ if server.Config == nil {
+ server.Config = map[string]interface{}{}
+ }
+
+ server.Config[key] = value
+
+ err = c.UpdateServer(server.Writable(), etag)
+ if err != nil {
+ return err
+ }
+
+ return nil
+ }
+
+ profileDeviceAdd := func(profileName string, deviceName string, deviceConfig map[string]string) error {
+ profile, etag, err := c.GetProfile(profileName)
+ if err != nil {
+ return err
+ }
+
+ if profile.Devices == nil {
+ profile.Devices = map[string]map[string]string{}
+ }
+
+ _, ok := profile.Devices[deviceName]
+ if ok {
+ return fmt.Errorf("Device already exists: %s", deviceName)
+ }
+
+ profile.Devices[deviceName] = deviceConfig
+
+ err = c.UpdateProfile(profileName, profile.Writable(), etag)
+ if err != nil {
+ return err
+ }
+
+ return nil
+ }
+
+ setProfileConfigItem := func(profileName string, key string, value string) error {
+ profile, etag, err := c.GetProfile(profileName)
+ if err != nil {
+ return err
+ }
+
+ if profile.Config == nil {
+ profile.Config = map[string]string{}
+ }
+
+ profile.Config[key] = value
+
+ err = c.UpdateProfile(profileName, profile.Writable(), etag)
+ if err != nil {
+ return err
+ }
+
+ return nil
+ }
+
+ pools, err := c.GetStoragePoolNames()
if err != nil {
// We should consider this fatal since this means
// something's wrong with the daemon.
@@ -236,8 +302,7 @@ func cmdInit() error {
storageSetup = askBool("Do you want to configure a new storage pool (yes/no) [default=yes]? ", "yes")
if storageSetup {
storagePool = askString("Name of the new storage pool [default=default]: ", "default", nil)
- _, err := c.StoragePoolGet(storagePool)
- if err == nil {
+ if shared.StringInSlice(storagePool, pools) {
fmt.Printf("The requested storage pool \"%s\" already exists. Please choose another name.\n", storagePool)
// Ask the user again if hew wants to create a
// storage pool.
@@ -346,7 +411,7 @@ they otherwise would.
bridgeName = ""
if askBool("Would you like to create a new network bridge (yes/no) [default=yes]? ", "yes") {
bridgeName = askString("What should the new bridge be called [default=lxdbr0]? ", "lxdbr0", networkValidName)
- _, err := c.NetworkGet(bridgeName)
+ _, _, err := c.GetNetwork(bridgeName)
if err == nil {
fmt.Printf("The requested network bridge \"%s\" already exists. Please choose another name.\n", bridgeName)
// Ask the user again if hew wants to create a
@@ -381,7 +446,7 @@ they otherwise would.
if storageSetup {
// Unset core.https_address and core.trust_password
for _, key := range []string{"core.https_address", "core.trust_password"} {
- _, err = c.SetServerConfig(key, "")
+ err = setServerConfig(key, "")
if err != nil {
return err
}
@@ -408,7 +473,13 @@ they otherwise would.
}
// Create the requested storage pool.
- err := c.StoragePoolCreate(storagePool, storageBackend, storagePoolConfig)
+ storageStruct := api.StoragePoolsPost{
+ Name: storagePool,
+ Driver: storageBackend,
+ }
+ storageStruct.Config = storagePoolConfig
+
+ err := c.CreateStoragePool(storageStruct)
if err != nil {
return err
}
@@ -418,7 +489,7 @@ they otherwise would.
// default profile again. Let the user figure this out.
if len(pools) == 0 {
// Check if there even is a default profile.
- profiles, err := c.ListProfiles()
+ profiles, err := c.GetProfiles()
if err != nil {
return err
}
@@ -441,10 +512,11 @@ they otherwise would.
// the default profile it must be empty otherwise it would
// not have been possible to delete the storage pool in
// the first place.
- p.ProfilePut.Devices[k]["pool"] = storagePool
+ update := p.Writable()
+ update.Devices[k]["pool"] = storagePool
// Update profile devices.
- err := c.PutProfile("default", p.ProfilePut)
+ err := c.UpdateProfile("default", update, "")
if err != nil {
return err
}
@@ -458,11 +530,17 @@ they otherwise would.
break
}
- props := []string{"path=/", fmt.Sprintf("pool=%s", storagePool)}
- _, err = c.ProfileDeviceAdd("default", "root", "disk", props)
+ props := map[string]string{
+ "type": "disk",
+ "path": "/",
+ "pool": storagePool,
+ }
+
+ err = profileDeviceAdd("default", "root", props)
if err != nil {
return err
}
+
break
}
@@ -473,42 +551,43 @@ they otherwise would.
}
if defaultPrivileged == 0 {
- err = c.SetProfileConfigItem("default", "security.privileged", "")
+ err = setProfileConfigItem("default", "security.privileged", "")
if err != nil {
return err
}
} else if defaultPrivileged == 1 {
- err = c.SetProfileConfigItem("default", "security.privileged", "true")
+ err = setProfileConfigItem("default", "security.privileged", "true")
if err != nil {
}
}
if imagesAutoUpdate {
- ss, err := c.ServerStatus()
+ ss, _, err := c.GetServer()
if err != nil {
return err
}
+
if val, ok := ss.Config["images.auto_update_interval"]; ok && val == "0" {
- _, err = c.SetServerConfig("images.auto_update_interval", "")
+ err = setServerConfig("images.auto_update_interval", "")
if err != nil {
return err
}
}
} else {
- _, err = c.SetServerConfig("images.auto_update_interval", "0")
+ err = setServerConfig("images.auto_update_interval", "0")
if err != nil {
return err
}
}
if networkAddress != "" {
- _, err = c.SetServerConfig("core.https_address", fmt.Sprintf("%s:%d", networkAddress, networkPort))
+ err = setServerConfig("core.https_address", fmt.Sprintf("%s:%d", networkAddress, networkPort))
if err != nil {
return err
}
if trustPassword != "" {
- _, err = c.SetServerConfig("core.trust_password", trustPassword)
+ err = setServerConfig("core.trust_password", trustPassword)
if err != nil {
return err
}
@@ -528,13 +607,22 @@ they otherwise would.
bridgeConfig["ipv6.nat"] = "true"
}
- err = c.NetworkCreate(bridgeName, bridgeConfig)
+ network := api.NetworksPost{
+ Name: bridgeName}
+ network.Config = bridgeConfig
+
+ err = c.CreateNetwork(network)
if err != nil {
return err
}
- props := []string{"nictype=bridged", fmt.Sprintf("parent=%s", bridgeName)}
- _, err = c.ProfileDeviceAdd("default", "eth0", "nic", props)
+ props := map[string]string{
+ "type": "nic",
+ "nictype": "bridged",
+ "parent": bridgeName,
+ }
+
+ err = profileDeviceAdd("default", "eth0", props)
if err != nil {
return err
}
From c3e6ac0164dce0c1af938a1282af96b311f22b5b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 22 Mar 2017 21:46:23 -0400
Subject: [PATCH 13/15] Port main_shutdown to new 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>
---
lxd/main_shutdown.go | 25 +++++++++++++------------
1 file changed, 13 insertions(+), 12 deletions(-)
diff --git a/lxd/main_shutdown.go b/lxd/main_shutdown.go
index 74c380f..56be2aa 100644
--- a/lxd/main_shutdown.go
+++ b/lxd/main_shutdown.go
@@ -2,10 +2,9 @@ package main
import (
"fmt"
- "net/http"
"time"
- "github.com/lxc/lxd"
+ "github.com/lxc/lxd/client"
)
func cmdShutdown() error {
@@ -17,28 +16,30 @@ func cmdShutdown() error {
timeout = *argTimeout
}
- c, err := lxd.NewClient(&lxd.DefaultConfig, "local")
+ c, err := lxd.ConnectLXDUnix("", nil)
if err != nil {
return err
}
- req, err := http.NewRequest("PUT", c.BaseURL+"/internal/shutdown", nil)
+ _, _, err = c.RawQuery("PUT", "/internal/shutdown", nil, "")
if err != nil {
return err
}
- _, err = c.Http.Do(req)
- if err != nil {
- return err
- }
-
- monitor := make(chan error, 1)
+ chMonitor := make(chan bool, 1)
go func() {
- monitor <- c.Monitor(nil, func(m interface{}) {}, nil)
+ monitor, err := c.GetEvents()
+ if err != nil {
+ close(chMonitor)
+ return
+ }
+
+ monitor.Wait()
+ close(chMonitor)
}()
select {
- case <-monitor:
+ case <-chMonitor:
break
case <-time.After(time.Second * time.Duration(timeout)):
return fmt.Errorf("LXD still running after %ds timeout.", timeout)
From 72b540873bc67072dde24f2bd6e8ed7e2e45cc1b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 22 Mar 2017 23:30:28 -0400
Subject: [PATCH 14/15] Port main_migratedumpsuccess to new 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>
---
lxd/main_migratedumpsuccess.go | 24 ++++++++++++++++++++----
1 file changed, 20 insertions(+), 4 deletions(-)
diff --git a/lxd/main_migratedumpsuccess.go b/lxd/main_migratedumpsuccess.go
index c4c8fb1..663e6f9 100644
--- a/lxd/main_migratedumpsuccess.go
+++ b/lxd/main_migratedumpsuccess.go
@@ -2,8 +2,10 @@ package main
import (
"fmt"
+ "net/url"
- "github.com/lxc/lxd"
+ "github.com/lxc/lxd/client"
+ "github.com/lxc/lxd/shared/api"
)
func cmdMigrateDumpSuccess(args []string) error {
@@ -11,16 +13,30 @@ func cmdMigrateDumpSuccess(args []string) error {
return fmt.Errorf("bad migrate dump success args %s", args)
}
- c, err := lxd.NewClient(&lxd.DefaultConfig, "local")
+ c, err := lxd.ConnectLXDUnix("", nil)
if err != nil {
return err
}
- conn, err := c.Websocket(args[1], args[2])
+ conn, err := c.RawWebsocket(fmt.Sprintf("/1.0/operations/%s/websocket?%s", args[1], url.Values{"secret": []string{args[2]}}))
if err != nil {
return err
}
conn.Close()
- return c.WaitForSuccess(args[1])
+ resp, _, err := c.RawQuery("GET", fmt.Sprintf("/1.0/operations/%s/wait", args[1]), nil, "")
+ if err != nil {
+ return err
+ }
+
+ op, err := resp.MetadataAsOperation()
+ if err != nil {
+ return err
+ }
+
+ if op.StatusCode == api.Success {
+ return nil
+ }
+
+ return fmt.Errorf(op.Err)
}
From c3a3b59124d7a582c31c76abb97e203ea70037f2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Thu, 23 Mar 2017 22:53:56 -0400
Subject: [PATCH 15/15] Port lxd-benchmark to new 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>
---
test/lxd-benchmark/main.go | 84 +++++++++++++++++++++++++++++++---------------
1 file changed, 57 insertions(+), 27 deletions(-)
diff --git a/test/lxd-benchmark/main.go b/test/lxd-benchmark/main.go
index a6c9544..6861462 100644
--- a/test/lxd-benchmark/main.go
+++ b/test/lxd-benchmark/main.go
@@ -8,7 +8,8 @@ import (
"sync"
"time"
- "github.com/lxc/lxd"
+ "github.com/lxc/lxd/client"
+ "github.com/lxc/lxd/lxc/config"
"github.com/lxc/lxd/shared"
"github.com/lxc/lxd/shared/api"
"github.com/lxc/lxd/shared/gnuflag"
@@ -60,7 +61,7 @@ func run(args []string) error {
gnuflag.Parse(true)
// Connect to LXD
- c, err := lxd.NewClient(&lxd.DefaultConfig, "local")
+ c, err := lxd.ConnectLXDUnix("", nil)
if err != nil {
return err
}
@@ -79,7 +80,7 @@ func logf(format string, args ...interface{}) {
fmt.Printf(fmt.Sprintf("[%s] %s\n", time.Now().Format(time.StampMilli), format), args...)
}
-func spawnContainers(c *lxd.Client, count int, image string, privileged bool) error {
+func spawnContainers(c lxd.ContainerServer, count int, image string, privileged bool) error {
batch := *argParallel
if batch < 1 {
// Detect the number of parallel actions
@@ -95,7 +96,7 @@ func spawnContainers(c *lxd.Client, count int, image string, privileged bool) er
remainder := count % batch
// Print the test header
- st, err := c.ServerStatus()
+ st, _, err := c.GetServer()
if err != nil {
return err
}
@@ -135,27 +136,47 @@ func spawnContainers(c *lxd.Client, count int, image string, privileged bool) er
var fingerprint string
if strings.Contains(image, ":") {
var remote string
- remote, fingerprint = lxd.DefaultConfig.ParseRemoteAndContainer(image)
- if fingerprint == "" {
- fingerprint = "default"
+ defaultConfig := config.DefaultConfig
+ defaultConfig.UserAgent = version.UserAgent
+
+ remote, fingerprint, err = defaultConfig.ParseRemote(image)
+ if err != nil {
+ return err
}
- d, err := lxd.NewClient(&lxd.DefaultConfig, remote)
+ d, err := defaultConfig.GetImageServer(remote)
if err != nil {
return err
}
- target := d.GetAlias(fingerprint)
- if target != "" {
- fingerprint = target
+ if fingerprint == "" {
+ fingerprint = "default"
}
- _, err = c.GetImageInfo(fingerprint)
+ alias, _, err := d.GetImageAlias(fingerprint)
+ if err == nil {
+ fingerprint = alias.Target
+ }
+
+ _, _, err = c.GetImage(fingerprint)
if err != nil {
logf("Importing image into local store: %s", fingerprint)
- err := d.CopyImage(fingerprint, c, false, nil, false, false, nil)
+ image, _, err := d.GetImage(fingerprint)
if err != nil {
+ logf(fmt.Sprintf("Failed to import image: %s", err))
+ return err
+ }
+
+ op, err := d.CopyImage(*image, c, nil)
+ if err != nil {
+ logf(fmt.Sprintf("Failed to import image: %s", err))
+ return err
+ }
+
+ err = op.Wait()
+ if err != nil {
+ logf(fmt.Sprintf("Failed to import image: %s", err))
return err
}
} else {
@@ -183,26 +204,35 @@ func spawnContainers(c *lxd.Client, count int, image string, privileged bool) er
config["user.lxd-benchmark"] = "true"
// Create
- resp, err := c.Init(name, "local", fingerprint, nil, config, nil, false)
+ req := api.ContainersPost{
+ Name: name,
+ Source: api.ContainerSource{
+ Type: "image",
+ Fingerprint: fingerprint,
+ },
+ }
+ req.Config = config
+
+ op, err := c.CreateContainer(req)
if err != nil {
logf(fmt.Sprintf("Failed to spawn container '%s': %s", name, err))
return
}
- err = c.WaitForSuccess(resp.Operation)
+ err = op.Wait()
if err != nil {
logf(fmt.Sprintf("Failed to spawn container '%s': %s", name, err))
return
}
// Start
- resp, err = c.Action(name, "start", -1, false, false)
+ op, err = c.UpdateContainerState(name, api.ContainerStatePut{Action: "start", Timeout: -1}, "")
if err != nil {
logf(fmt.Sprintf("Failed to spawn container '%s': %s", name, err))
return
}
- err = c.WaitForSuccess(resp.Operation)
+ err = op.Wait()
if err != nil {
logf(fmt.Sprintf("Failed to spawn container '%s': %s", name, err))
return
@@ -210,13 +240,13 @@ func spawnContainers(c *lxd.Client, count int, image string, privileged bool) er
// Freeze
if *argFreeze {
- resp, err = c.Action(name, "freeze", -1, false, false)
+ op, err := c.UpdateContainerState(name, api.ContainerStatePut{Action: "freeze", Timeout: -1}, "")
if err != nil {
logf(fmt.Sprintf("Failed to spawn container '%s': %s", name, err))
return
}
- err = c.WaitForSuccess(resp.Operation)
+ err = op.Wait()
if err != nil {
logf(fmt.Sprintf("Failed to spawn container '%s': %s", name, err))
return
@@ -258,7 +288,7 @@ func spawnContainers(c *lxd.Client, count int, image string, privileged bool) er
return nil
}
-func deleteContainers(c *lxd.Client) error {
+func deleteContainers(c lxd.ContainerServer) error {
batch := *argParallel
if batch < 1 {
// Detect the number of parallel actions
@@ -271,7 +301,7 @@ func deleteContainers(c *lxd.Client) error {
}
// List all the containers
- allContainers, err := c.ListContainers()
+ allContainers, err := c.GetContainers()
if err != nil {
return err
}
@@ -300,27 +330,27 @@ func deleteContainers(c *lxd.Client) error {
// Stop
if ct.IsActive() {
- resp, err := c.Action(ct.Name, "stop", -1, true, false)
+ op, err := c.UpdateContainerState(ct.Name, api.ContainerStatePut{Action: "stop", Timeout: -1, Force: true}, "")
if err != nil {
- logf("Failed to delete container: %s", ct.Name)
+ logf(fmt.Sprintf("Failed to delete container '%s': %s", ct.Name, err))
return
}
- err = c.WaitForSuccess(resp.Operation)
+ err = op.Wait()
if err != nil {
- logf("Failed to delete container: %s", ct.Name)
+ logf(fmt.Sprintf("Failed to delete container '%s': %s", ct.Name, err))
return
}
}
// Delete
- resp, err := c.Delete(ct.Name)
+ op, err := c.DeleteContainer(ct.Name)
if err != nil {
logf("Failed to delete container: %s", ct.Name)
return
}
- err = c.WaitForSuccess(resp.Operation)
+ err = op.Wait()
if err != nil {
logf("Failed to delete container: %s", ct.Name)
return
More information about the lxc-devel
mailing list