[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