[lxc-devel] [lxd/master] Client Instances 2 - rest

tomponline on Github lxc-bot at linuxcontainers.org
Thu Sep 12 14:55:02 UTC 2019


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 557 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20190912/285ce312/attachment-0001.bin>
-------------- next part --------------
From d7d30a08104061b1f74a51c92dcfcf184cd0c899 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Thu, 12 Sep 2019 11:57:02 +0100
Subject: [PATCH 1/6] client/connection: Adds connect functions that return
 InstanceServer type

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 client/connection.go | 30 +++++++++++++++++++++++++-----
 1 file changed, 25 insertions(+), 5 deletions(-)

diff --git a/client/connection.go b/client/connection.go
index c9a25e1066..6cfa0fb199 100644
--- a/client/connection.go
+++ b/client/connection.go
@@ -51,14 +51,14 @@ type ConnectionArgs struct {
 	SkipGetServer bool
 }
 
-// ConnectLXD lets you connect to a remote LXD daemon over HTTPs.
+// ConnectLXDInstance 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) {
+func ConnectLXDInstance(url string, args *ConnectionArgs) (InstanceServer, error) {
 	logger.Debugf("Connecting to a remote LXD over HTTPs")
 
 	// Cleanup URL
@@ -67,12 +67,23 @@ func ConnectLXD(url string, args *ConnectionArgs) (ContainerServer, error) {
 	return httpsLXD(url, args)
 }
 
-// ConnectLXDUnix lets you connect to a remote LXD daemon over a local unix socket.
+// 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) {
+	return ConnectLXDInstance(url, args)
+}
+
+// ConnectLXDInstanceUnix lets you connect to a remote LXD daemon over a local unix socket.
 //
 // If the path argument is empty, then $LXD_SOCKET will be used, if
 // unset $LXD_DIR/unix.socket will be used and 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) {
+func ConnectLXDInstanceUnix(path string, args *ConnectionArgs) (InstanceServer, error) {
 	logger.Debugf("Connecting to a local LXD over a Unix socket")
 
 	// Use empty args if not specified
@@ -122,6 +133,15 @@ func ConnectLXDUnix(path string, args *ConnectionArgs) (ContainerServer, error)
 	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_SOCKET will be used, if
+// unset $LXD_DIR/unix.socket will be used and 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) {
+	return ConnectLXDInstanceUnix(path, args)
+}
+
 // 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).
@@ -170,7 +190,7 @@ func ConnectSimpleStreams(url string, args *ConnectionArgs) (ImageServer, error)
 }
 
 // Internal function called by ConnectLXD and ConnectPublicLXD
-func httpsLXD(url string, args *ConnectionArgs) (ContainerServer, error) {
+func httpsLXD(url string, args *ConnectionArgs) (InstanceServer, error) {
 	// Use empty args if not specified
 	if args == nil {
 		args = &ConnectionArgs{}

From cd9619a83512a4ee03047fa2241531945abe2889 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Thu, 12 Sep 2019 11:57:49 +0100
Subject: [PATCH 2/6] client/interfaces: Creates InstanceServer interface

- Creates new arg types for instances and aliases existing types.

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 client/interfaces.go | 68 +++++++++++++++++++++++++++++++++-----------
 1 file changed, 52 insertions(+), 16 deletions(-)

diff --git a/client/interfaces.go b/client/interfaces.go
index 6e8cc7df3e..ae5cf82c9b 100644
--- a/client/interfaces.go
+++ b/client/interfaces.go
@@ -233,6 +233,16 @@ type ContainerServer interface {
 	RawOperation(method string, path string, data interface{}, queryETag string) (op Operation, ETag string, err error)
 }
 
+// The InstanceServer type represents a full featured LXD server with Instances support.
+//
+// API extension: instances
+type InstanceServer interface {
+	ContainerServer
+
+	UseInstanceTarget(name string) (client InstanceServer)
+	UseInstanceProject(name string) (client InstanceServer)
+}
+
 // The ConnectionInfo struct represents general information for a connection
 type ConnectionInfo struct {
 	Addresses   []string
@@ -243,8 +253,8 @@ type ConnectionInfo struct {
 	Project     string
 }
 
-// The ContainerBackupArgs struct is used when creating a container from a backup
-type ContainerBackupArgs struct {
+// The InstanceBackupArgs struct is used when creating a container from a backup
+type InstanceBackupArgs struct {
 	// The backup file
 	BackupFile io.Reader
 
@@ -252,6 +262,9 @@ type ContainerBackupArgs struct {
 	PoolName string
 }
 
+// The ContainerBackupArgs struct is used when creating a container from a backup
+type ContainerBackupArgs InstanceBackupArgs
+
 // The BackupFileRequest struct is used for a backup download request
 type BackupFileRequest struct {
 	// Writer for the backup file
@@ -356,8 +369,8 @@ type StoragePoolVolumeMoveArgs struct {
 	StoragePoolVolumeCopyArgs
 }
 
-// The ContainerCopyArgs struct is used to pass additional options during container copy
-type ContainerCopyArgs struct {
+// The InstanceCopyArgs struct is used to pass additional options during container copy
+type InstanceCopyArgs struct {
 	// If set, the container will be renamed on copy
 	Name string
 
@@ -375,8 +388,11 @@ type ContainerCopyArgs struct {
 	Refresh bool
 }
 
-// The ContainerSnapshotCopyArgs struct is used to pass additional options during container copy
-type ContainerSnapshotCopyArgs struct {
+// The ContainerCopyArgs struct is used to pass additional options during container copy
+type ContainerCopyArgs InstanceCopyArgs
+
+// The InstanceSnapshotCopyArgs struct is used to pass additional options during container copy
+type InstanceSnapshotCopyArgs struct {
 	// If set, the container will be renamed on copy
 	Name string
 
@@ -388,9 +404,12 @@ type ContainerSnapshotCopyArgs struct {
 	Live bool
 }
 
-// The ContainerConsoleArgs struct is used to pass additional options during a
+// The ContainerSnapshotCopyArgs struct is used to pass additional options during container copy
+type ContainerSnapshotCopyArgs InstanceSnapshotCopyArgs
+
+// The InstanceConsoleArgs struct is used to pass additional options during a
 // container console session
-type ContainerConsoleArgs struct {
+type InstanceConsoleArgs struct {
 	// Bidirectional fd to pass to the container
 	Terminal io.ReadWriteCloser
 
@@ -401,13 +420,21 @@ type ContainerConsoleArgs struct {
 	ConsoleDisconnect chan bool
 }
 
-// The ContainerConsoleLogArgs struct is used to pass additional options during a
+// The ContainerConsoleArgs struct is used to pass additional options during a
+// container console session
+type ContainerConsoleArgs InstanceConsoleArgs
+
+// The InstanceConsoleLogArgs struct is used to pass additional options during a
 // container console log request
-type ContainerConsoleLogArgs struct {
+type InstanceConsoleLogArgs struct {
 }
 
-// The ContainerExecArgs struct is used to pass additional options during container exec
-type ContainerExecArgs struct {
+// The ContainerConsoleLogArgs struct is used to pass additional options during a
+// container console log request
+type ContainerConsoleLogArgs InstanceConsoleLogArgs
+
+// The InstanceExecArgs struct is used to pass additional options during container exec
+type InstanceExecArgs struct {
 	// Standard input
 	Stdin io.ReadCloser
 
@@ -424,8 +451,11 @@ type ContainerExecArgs struct {
 	DataDone chan bool
 }
 
-// The ContainerFileArgs struct is used to pass the various options for a container file upload
-type ContainerFileArgs struct {
+// The ContainerExecArgs struct is used to pass additional options during container exec
+type ContainerExecArgs InstanceExecArgs
+
+// The InstanceFileArgs struct is used to pass the various options for a container file upload
+type InstanceFileArgs struct {
 	// File content
 	Content io.ReadSeeker
 
@@ -445,8 +475,11 @@ type ContainerFileArgs struct {
 	WriteMode string
 }
 
-// The ContainerFileResponse struct is used as part of the response for a container file download
-type ContainerFileResponse struct {
+// The ContainerFileArgs struct is used to pass the various options for a container file upload
+type ContainerFileArgs InstanceFileArgs
+
+// The InstanceFileResponse struct is used as part of the response for a container file download
+type InstanceFileResponse struct {
 	// User id that owns the file
 	UID int64
 
@@ -462,3 +495,6 @@ type ContainerFileResponse struct {
 	// If a directory, the list of files inside it
 	Entries []string
 }
+
+// The ContainerFileResponse struct is used as part of the response for a container file download
+type ContainerFileResponse InstanceFileResponse

From 7676ee551c5e54f0c05e1ccb95ecabd24109c6c7 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Thu, 12 Sep 2019 11:58:41 +0100
Subject: [PATCH 3/6] client/lxd/server: Adds UseInstanceTarget and
 UseInstanceProject functions

And aliases existing functions to return old type.

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 client/lxd_server.go | 20 ++++++++++++++++----
 1 file changed, 16 insertions(+), 4 deletions(-)

diff --git a/client/lxd_server.go b/client/lxd_server.go
index 4cc20cc1c6..c1f3ec656c 100644
--- a/client/lxd_server.go
+++ b/client/lxd_server.go
@@ -89,8 +89,8 @@ func (r *ProtocolLXD) GetServerResources() (*api.Resources, error) {
 	return &resources, nil
 }
 
-// UseProject returns a client that will use a specific project.
-func (r *ProtocolLXD) UseProject(name string) ContainerServer {
+// UseInstanceProject returns a client that will use a specific project.
+func (r *ProtocolLXD) UseInstanceProject(name string) InstanceServer {
 	return &ProtocolLXD{
 		server:               r.server,
 		http:                 r.http,
@@ -106,10 +106,15 @@ func (r *ProtocolLXD) UseProject(name string) ContainerServer {
 	}
 }
 
-// UseTarget returns a client that will target a specific cluster member.
+// UseProject returns a client that will use a specific project.
+func (r *ProtocolLXD) UseProject(name string) ContainerServer {
+	return ContainerServer(r.UseInstanceProject(name))
+}
+
+// UseInstanceTarget returns a client that will target a specific cluster member.
 // Use this member-specific operations such as specific container
 // placement, preparing a new storage pool or network, ...
-func (r *ProtocolLXD) UseTarget(name string) ContainerServer {
+func (r *ProtocolLXD) UseInstanceTarget(name string) InstanceServer {
 	return &ProtocolLXD{
 		server:               r.server,
 		http:                 r.http,
@@ -124,3 +129,10 @@ func (r *ProtocolLXD) UseTarget(name string) ContainerServer {
 		clusterTarget:        name,
 	}
 }
+
+// UseTarget returns a client that will target a specific cluster member.
+// Use this member-specific operations such as specific container
+// placement, preparing a new storage pool or network, ...
+func (r *ProtocolLXD) UseTarget(name string) ContainerServer {
+	return ContainerServer(r.UseInstanceTarget(name))
+}

From 8bf45f7e90846a938f690501afdd2047ab2dbac1 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Thu, 12 Sep 2019 15:46:44 +0100
Subject: [PATCH 4/6] client/interfaces: Populates InstanceServer with rest of
 functions

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 client/interfaces.go | 59 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 59 insertions(+)

diff --git a/client/interfaces.go b/client/interfaces.go
index ae5cf82c9b..7ffce04c88 100644
--- a/client/interfaces.go
+++ b/client/interfaces.go
@@ -241,6 +241,65 @@ type InstanceServer interface {
 
 	UseInstanceTarget(name string) (client InstanceServer)
 	UseInstanceProject(name string) (client InstanceServer)
+
+	// Instance functions.
+	GetInstanceNames() (names []string, err error)
+	GetInstances() (instances []api.Instance, err error)
+	GetInstancesFull() (instances []api.InstanceFull, err error)
+	GetInstance(name string) (instance *api.Instance, ETag string, err error)
+	CreateInstance(instance api.InstancesPost) (op Operation, err error)
+	CreateInstanceFromImage(source ImageServer, image api.Image, imgcontainer api.InstancesPost) (op RemoteOperation, err error)
+	CopyInstance(source InstanceServer, instance api.Instance, args *InstanceCopyArgs) (op RemoteOperation, err error)
+	UpdateInstance(name string, instance api.InstancePut, ETag string) (op Operation, err error)
+	RenameInstance(name string, instance api.InstancePost) (op Operation, err error)
+	MigrateInstance(name string, instance api.InstancePost) (op Operation, err error)
+	DeleteInstance(name string) (op Operation, err error)
+
+	ExecInstance(instanceName string, exec api.InstanceExecPost, args *InstanceExecArgs) (op Operation, err error)
+	ConsoleInstance(instanceName string, console api.InstanceConsolePost, args *InstanceConsoleArgs) (op Operation, err error)
+	GetInstanceConsoleLog(instanceName string, args *InstanceConsoleLogArgs) (content io.ReadCloser, err error)
+	DeleteInstanceConsoleLog(instanceName string, args *InstanceConsoleLogArgs) (err error)
+
+	GetInstanceFile(instanceName string, path string) (content io.ReadCloser, resp *InstanceFileResponse, err error)
+	CreateInstanceFile(instanceName string, path string, args InstanceFileArgs) (err error)
+	DeleteInstanceFile(instanceName string, path string) (err error)
+
+	GetInstanceSnapshotNames(instanceName string) (names []string, err error)
+	GetInstanceSnapshots(instanceName string) (snapshots []api.InstanceSnapshot, err error)
+	GetInstanceSnapshot(instanceName string, name string) (snapshot *api.InstanceSnapshot, ETag string, err error)
+	CreateInstanceSnapshot(instanceName string, snapshot api.InstanceSnapshotsPost) (op Operation, err error)
+	CopyInstanceSnapshot(source InstanceServer, instanceName string, snapshot api.InstanceSnapshot, args *InstanceSnapshotCopyArgs) (op RemoteOperation, err error)
+	RenameInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPost) (op Operation, err error)
+	MigrateInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPost) (op Operation, err error)
+	DeleteInstanceSnapshot(instanceName string, name string) (op Operation, err error)
+	UpdateInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPut, ETag string) (op Operation, err error)
+
+	GetInstanceBackupNames(instanceName string) (names []string, err error)
+	GetInstanceBackups(instanceName string) (backups []api.InstanceBackup, err error)
+	GetInstanceBackup(instanceName string, name string) (backup *api.InstanceBackup, ETag string, err error)
+	CreateInstanceBackup(instanceName string, backup api.InstanceBackupsPost) (op Operation, err error)
+	RenameInstanceBackup(instanceName string, name string, backup api.InstanceBackupPost) (op Operation, err error)
+	DeleteInstanceBackup(instanceName string, name string) (op Operation, err error)
+	GetInstanceBackupFile(instanceName string, name string, req *BackupFileRequest) (resp *BackupFileResponse, err error)
+	CreateInstanceFromBackup(args InstanceBackupArgs) (op Operation, err error)
+
+	GetInstanceState(name string) (state *api.InstanceState, ETag string, err error)
+	UpdateInstanceState(name string, state api.InstanceStatePut, ETag string) (op Operation, err error)
+
+	GetInstanceLogfiles(name string) (logfiles []string, err error)
+	GetInstanceLogfile(name string, filename string) (content io.ReadCloser, err error)
+	DeleteInstanceLogfile(name string, filename string) (err error)
+
+	GetInstanceMetadata(name string) (metadata *api.ImageMetadata, ETag string, err error)
+	SetInstanceMetadata(name string, metadata api.ImageMetadata, ETag string) (err error)
+
+	GetInstanceTemplateFiles(instanceName string) (templates []string, err error)
+	GetInstanceTemplateFile(instanceName string, templateName string) (content io.ReadCloser, err error)
+	CreateInstanceTemplateFile(instanceName string, templateName string, content io.ReadSeeker) (err error)
+	UpdateInstanceTemplateFile(instanceName string, templateName string, content io.ReadSeeker) (err error)
+
+	CopyInstanceStoragePoolVolume(pool string, source InstanceServer, sourcePool string, volume api.StorageVolume, args *StoragePoolVolumeCopyArgs) (op RemoteOperation, err error)
+	MoveInstanceStoragePoolVolume(pool string, source InstanceServer, sourcePool string, volume api.StorageVolume, args *StoragePoolVolumeMoveArgs) (op RemoteOperation, err error)
 }
 
 // The ConnectionInfo struct represents general information for a connection

From c4adfff71afccca7c157d55633423130695751d0 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Thu, 12 Sep 2019 15:47:41 +0100
Subject: [PATCH 5/6] client/lxd/storage/volumes: Adds storage management shims
 to implement InstanceServer

- CopyInstanceStoragePoolVolume
- MoveInstanceStoragePoolVolume

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 client/lxd_storage_volumes.go | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/client/lxd_storage_volumes.go b/client/lxd_storage_volumes.go
index 291ad4f3e2..1f11296558 100644
--- a/client/lxd_storage_volumes.go
+++ b/client/lxd_storage_volumes.go
@@ -519,6 +519,11 @@ func (r *ProtocolLXD) CopyStoragePoolVolume(pool string, source ContainerServer,
 	return r.tryCreateStoragePoolVolume(pool, req, info.Addresses)
 }
 
+// CopyInstanceStoragePoolVolume copies an existing instance server storage volume.
+func (r *ProtocolLXD) CopyInstanceStoragePoolVolume(pool string, source InstanceServer, sourcePool string, volume api.StorageVolume, args *StoragePoolVolumeCopyArgs) (RemoteOperation, error) {
+	return r.CopyStoragePoolVolume(pool, source, sourcePool, volume, args)
+}
+
 // MoveStoragePoolVolume renames or moves an existing storage volume
 func (r *ProtocolLXD) MoveStoragePoolVolume(pool string, source ContainerServer, sourcePool string, volume api.StorageVolume, args *StoragePoolVolumeMoveArgs) (RemoteOperation, error) {
 	if !r.HasExtension("storage_api_local_volume_handling") {
@@ -554,6 +559,11 @@ func (r *ProtocolLXD) MoveStoragePoolVolume(pool string, source ContainerServer,
 	return &rop, nil
 }
 
+// MoveInstanceStoragePoolVolume renames or moves an existing instance server storage volume.
+func (r *ProtocolLXD) MoveInstanceStoragePoolVolume(pool string, source InstanceServer, sourcePool string, volume api.StorageVolume, args *StoragePoolVolumeMoveArgs) (RemoteOperation, error) {
+	return r.MoveStoragePoolVolume(pool, source, sourcePool, volume, args)
+}
+
 // UpdateStoragePoolVolume updates the volume to match the provided StoragePoolVolume struct
 func (r *ProtocolLXD) UpdateStoragePoolVolume(pool string, volType string, name string, volume api.StorageVolumePut, ETag string) error {
 	if !r.HasExtension("storage") {

From 8f2d6659ad7e9cc13f5eb568ff55174b2621e380 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Thu, 12 Sep 2019 15:50:34 +0100
Subject: [PATCH 6/6] client/lxd/instances: Adds instance related functions

These functions use the /1.0/instances endpoints on the server.

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 client/lxd_instances.go | 1915 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 1915 insertions(+)
 create mode 100644 client/lxd_instances.go

diff --git a/client/lxd_instances.go b/client/lxd_instances.go
new file mode 100644
index 0000000000..911754542f
--- /dev/null
+++ b/client/lxd_instances.go
@@ -0,0 +1,1915 @@
+package lxd
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strings"
+
+	"github.com/gorilla/websocket"
+
+	"github.com/lxc/lxd/shared"
+	"github.com/lxc/lxd/shared/api"
+	"github.com/lxc/lxd/shared/cancel"
+	"github.com/lxc/lxd/shared/ioprogress"
+	"github.com/lxc/lxd/shared/units"
+)
+
+// Instance handling functions.
+
+// GetInstanceNames returns a list of instance names.
+func (r *ProtocolLXD) GetInstanceNames() ([]string, error) {
+	urls := []string{}
+
+	// Fetch the raw value
+	_, err := r.queryStruct("GET", "/instances", nil, "", &urls)
+	if err != nil {
+		return nil, err
+	}
+
+	// Parse it
+	names := []string{}
+	for _, url := range urls {
+		fields := strings.Split(url, "/instances/")
+		names = append(names, fields[len(fields)-1])
+	}
+
+	return names, nil
+}
+
+// GetInstances returns a list of instances.
+func (r *ProtocolLXD) GetInstances() ([]api.Instance, error) {
+	instances := []api.Instance{}
+
+	// Fetch the raw value
+	_, err := r.queryStruct("GET", "/instances?recursion=1", nil, "", &instances)
+	if err != nil {
+		return nil, err
+	}
+
+	return instances, nil
+}
+
+// GetInstancesFull returns a list of instances including snapshots, backups and state.
+func (r *ProtocolLXD) GetInstancesFull() ([]api.InstanceFull, error) {
+	instances := []api.InstanceFull{}
+
+	if !r.HasExtension("container_full") {
+		return nil, fmt.Errorf("The server is missing the required \"container_full\" API extension")
+	}
+
+	// Fetch the raw value
+	_, err := r.queryStruct("GET", "/instances?recursion=2", nil, "", &instances)
+	if err != nil {
+		return nil, err
+	}
+
+	return instances, nil
+}
+
+// GetInstance returns the instance entry for the provided name.
+func (r *ProtocolLXD) GetInstance(name string) (*api.Instance, string, error) {
+	instance := api.Instance{}
+
+	// Fetch the raw value
+	etag, err := r.queryStruct("GET", fmt.Sprintf("/instances/%s", url.PathEscape(name)), nil, "", &instance)
+	if err != nil {
+		return nil, "", err
+	}
+
+	return &instance, etag, nil
+}
+
+// CreateInstanceFromBackup is a convenience function to make it easier to
+// create a instance from a backup
+func (r *ProtocolLXD) CreateInstanceFromBackup(args InstanceBackupArgs) (Operation, error) {
+	if !r.HasExtension("container_backup") {
+		return nil, fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+	}
+
+	if args.PoolName == "" {
+		// Send the request
+		op, _, err := r.queryOperation("POST", "/instances", args.BackupFile, "")
+		if err != nil {
+			return nil, err
+		}
+
+		return op, nil
+	}
+
+	if !r.HasExtension("container_backup_override_pool") {
+		return nil, fmt.Errorf("The server is missing the required \"container_backup_override_pool\" API extension")
+	}
+
+	// Prepare the HTTP request
+	reqURL, err := r.setQueryAttributes(fmt.Sprintf("%s/1.0/instances", r.httpHost))
+	if err != nil {
+		return nil, err
+	}
+
+	req, err := http.NewRequest("POST", reqURL, args.BackupFile)
+	if err != nil {
+		return nil, err
+	}
+
+	req.Header.Set("Content-Type", "application/octet-stream")
+	req.Header.Set("X-LXD-pool", args.PoolName)
+
+	// Set the user agent
+	if r.httpUserAgent != "" {
+		req.Header.Set("User-Agent", r.httpUserAgent)
+	}
+
+	// Send the request
+	resp, err := r.do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	// Handle errors
+	response, _, err := lxdParseResponse(resp)
+	if err != nil {
+		return nil, err
+	}
+
+	// Get to the operation
+	respOperation, err := response.MetadataAsOperation()
+	if err != nil {
+		return nil, err
+	}
+
+	// Setup an Operation wrapper
+	op := operation{
+		Operation: *respOperation,
+		r:         r,
+		chActive:  make(chan bool),
+	}
+
+	return &op, nil
+}
+
+// CreateInstance requests that LXD creates a new instance.
+func (r *ProtocolLXD) CreateInstance(instance api.InstancesPost) (Operation, error) {
+	if instance.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", "/instances", instance, "")
+	if err != nil {
+		return nil, err
+	}
+
+	return op, nil
+}
+
+func (r *ProtocolLXD) tryCreateInstance(req api.InstancesPost, urls []string) (RemoteOperation, error) {
+	if len(urls) == 0 {
+		return nil, fmt.Errorf("The source server isn't listening on the network")
+	}
+
+	rop := remoteOperation{
+		chDone: make(chan bool),
+	}
+
+	operation := req.Source.Operation
+
+	// Forward targetOp to remote op
+	go func() {
+		success := false
+		errors := map[string]error{}
+		for _, serverURL := range urls {
+			if operation == "" {
+				req.Source.Server = serverURL
+			} else {
+				req.Source.Operation = fmt.Sprintf("%s/1.0/operations/%s", serverURL, url.PathEscape(operation))
+			}
+
+			op, err := r.CreateInstance(req)
+			if err != nil {
+				errors[serverURL] = err
+				continue
+			}
+
+			rop.targetOp = op
+
+			for _, handler := range rop.handlers {
+				rop.targetOp.AddHandler(handler)
+			}
+
+			err = rop.targetOp.Wait()
+			if err != nil {
+				errors[serverURL] = err
+				continue
+			}
+
+			success = true
+			break
+		}
+
+		if !success {
+			rop.err = remoteOperationError("Failed instance creation", errors)
+		}
+
+		close(rop.chDone)
+	}()
+
+	return &rop, nil
+}
+
+// CreateInstanceFromImage is a convenience function to make it easier to create a instance from an existing image.
+func (r *ProtocolLXD) CreateInstanceFromImage(source ImageServer, image api.Image, req api.InstancesPost) (RemoteOperation, error) {
+	// Set the minimal source fields
+	req.Source.Type = "image"
+
+	// Optimization for the local image case
+	if r == source {
+		// Always use fingerprints for local case
+		req.Source.Fingerprint = image.Fingerprint
+		req.Source.Alias = ""
+
+		op, err := r.CreateInstance(req)
+		if err != nil {
+			return nil, err
+		}
+
+		rop := remoteOperation{
+			targetOp: op,
+			chDone:   make(chan bool),
+		}
+
+		// Forward targetOp to remote op
+		go func() {
+			rop.err = rop.targetOp.Wait()
+			close(rop.chDone)
+		}()
+
+		return &rop, nil
+	}
+
+	// Minimal source fields for remote image
+	req.Source.Mode = "pull"
+
+	// If we have an alias and the image is public, use that
+	if req.Source.Alias != "" && image.Public {
+		req.Source.Fingerprint = ""
+	} else {
+		req.Source.Fingerprint = image.Fingerprint
+		req.Source.Alias = ""
+	}
+
+	// Get source server connection information
+	info, err := source.GetConnectionInfo()
+	if err != nil {
+		return nil, err
+	}
+
+	req.Source.Protocol = info.Protocol
+	req.Source.Certificate = info.Certificate
+
+	// Generate secret token if needed
+	if !image.Public {
+		secret, err := source.GetImageSecret(image.Fingerprint)
+		if err != nil {
+			return nil, err
+		}
+
+		req.Source.Secret = secret
+	}
+
+	return r.tryCreateInstance(req, info.Addresses)
+}
+
+// CopyInstance copies a instance from a remote server. Additional options can be passed using InstanceCopyArgs.
+func (r *ProtocolLXD) CopyInstance(source InstanceServer, instance api.Instance, args *InstanceCopyArgs) (RemoteOperation, error) {
+	// Base request
+	req := api.InstancesPost{
+		Name:        instance.Name,
+		InstancePut: instance.Writable(),
+	}
+	req.Source.BaseImage = instance.Config["volatile.base_image"]
+
+	// Process the copy arguments
+	if args != nil {
+		// Sanity checks
+		if args.ContainerOnly {
+			if !r.HasExtension("container_only_migration") {
+				return nil, fmt.Errorf("The target server is missing the required \"container_only_migration\" API extension")
+			}
+
+			if !source.HasExtension("container_only_migration") {
+				return nil, fmt.Errorf("The source server is missing the required \"container_only_migration\" API extension")
+			}
+		}
+
+		if shared.StringInSlice(args.Mode, []string{"push", "relay"}) {
+			if !r.HasExtension("container_push") {
+				return nil, fmt.Errorf("The target server is missing the required \"container_push\" API extension")
+			}
+
+			if !source.HasExtension("container_push") {
+				return nil, fmt.Errorf("The source server is missing the required \"container_push\" API extension")
+			}
+		}
+
+		if args.Mode == "push" && !source.HasExtension("container_push_target") {
+			return nil, fmt.Errorf("The source server is missing the required \"container_push_target\" API extension")
+		}
+
+		if args.Refresh {
+			if !r.HasExtension("container_incremental_copy") {
+				return nil, fmt.Errorf("The target server is missing the required \"container_incremental_copy\" API extension")
+			}
+
+			if !source.HasExtension("container_incremental_copy") {
+				return nil, fmt.Errorf("The source server is missing the required \"container_incremental_copy\" API extension")
+			}
+		}
+
+		// Allow overriding the target name
+		if args.Name != "" {
+			req.Name = args.Name
+		}
+
+		req.Source.Live = args.Live
+		req.Source.ContainerOnly = args.ContainerOnly
+		req.Source.Refresh = args.Refresh
+	}
+
+	if req.Source.Live {
+		req.Source.Live = instance.StatusCode == api.Running
+	}
+
+	sourceInfo, err := source.GetConnectionInfo()
+	if err != nil {
+		return nil, fmt.Errorf("Failed to get source connection info: %v", err)
+	}
+
+	destInfo, err := r.GetConnectionInfo()
+	if err != nil {
+		return nil, fmt.Errorf("Failed to get destination connection info: %v", err)
+	}
+
+	// Optimization for the local copy case
+	if destInfo.URL == sourceInfo.URL && destInfo.SocketPath == sourceInfo.SocketPath && (!r.IsClustered() || instance.Location == r.clusterTarget || r.HasExtension("cluster_internal_copy")) {
+		// Project handling
+		if destInfo.Project != sourceInfo.Project {
+			if !r.HasExtension("container_copy_project") {
+				return nil, fmt.Errorf("The server is missing the required \"container_copy_project\" API extension")
+			}
+
+			req.Source.Project = sourceInfo.Project
+		}
+
+		// Local copy source fields
+		req.Source.Type = "copy"
+		req.Source.Source = instance.Name
+
+		// Copy the instance
+		op, err := r.CreateInstance(req)
+		if err != nil {
+			return nil, err
+		}
+
+		rop := remoteOperation{
+			targetOp: op,
+			chDone:   make(chan bool),
+		}
+
+		// Forward targetOp to remote op
+		go func() {
+			rop.err = rop.targetOp.Wait()
+			close(rop.chDone)
+		}()
+
+		return &rop, nil
+	}
+
+	// Source request
+	sourceReq := api.InstancePost{
+		Migration:     true,
+		Live:          req.Source.Live,
+		ContainerOnly: req.Source.ContainerOnly,
+	}
+
+	// Push mode migration
+	if args != nil && args.Mode == "push" {
+		// Get target server connection information
+		info, err := r.GetConnectionInfo()
+		if err != nil {
+			return nil, err
+		}
+
+		// Create the instance
+		req.Source.Type = "migration"
+		req.Source.Mode = "push"
+		req.Source.Refresh = args.Refresh
+
+		op, err := r.CreateInstance(req)
+		if err != nil {
+			return nil, err
+		}
+
+		opAPI := op.Get()
+
+		targetSecrets := map[string]string{}
+		for k, v := range opAPI.Metadata {
+			targetSecrets[k] = v.(string)
+		}
+
+		// Prepare the source request
+		target := api.InstancePostTarget{}
+		target.Operation = opAPI.ID
+		target.Websockets = targetSecrets
+		target.Certificate = info.Certificate
+		sourceReq.Target = &target
+
+		return r.tryMigrateInstance(source, instance.Name, sourceReq, info.Addresses)
+	}
+
+	// Get source server connection information
+	info, err := source.GetConnectionInfo()
+	if err != nil {
+		return nil, err
+	}
+
+	op, err := source.MigrateInstance(instance.Name, sourceReq)
+	if err != nil {
+		return nil, err
+	}
+	opAPI := op.Get()
+
+	sourceSecrets := map[string]string{}
+	for k, v := range opAPI.Metadata {
+		sourceSecrets[k] = v.(string)
+	}
+
+	// Relay mode migration
+	if args != nil && args.Mode == "relay" {
+		// Push copy source fields
+		req.Source.Type = "migration"
+		req.Source.Mode = "push"
+
+		// Start the process
+		targetOp, err := r.CreateInstance(req)
+		if err != nil {
+			return nil, err
+		}
+		targetOpAPI := targetOp.Get()
+
+		// Extract the websockets
+		targetSecrets := map[string]string{}
+		for k, v := range targetOpAPI.Metadata {
+			targetSecrets[k] = v.(string)
+		}
+
+		// Launch the relay
+		err = r.proxyMigration(targetOp.(*operation), targetSecrets, source, op.(*operation), sourceSecrets)
+		if err != nil {
+			return nil, err
+		}
+
+		// Prepare a tracking operation
+		rop := remoteOperation{
+			targetOp: targetOp,
+			chDone:   make(chan bool),
+		}
+
+		// Forward targetOp to remote op
+		go func() {
+			rop.err = rop.targetOp.Wait()
+			close(rop.chDone)
+		}()
+
+		return &rop, nil
+	}
+
+	// Pull mode migration
+	req.Source.Type = "migration"
+	req.Source.Mode = "pull"
+	req.Source.Operation = opAPI.ID
+	req.Source.Websockets = sourceSecrets
+	req.Source.Certificate = info.Certificate
+
+	return r.tryCreateInstance(req, info.Addresses)
+}
+
+func (r *ProtocolLXD) proxyInstanceMigration(targetOp *operation, targetSecrets map[string]string, source InstanceServer, sourceOp *operation, sourceSecrets map[string]string) error {
+	// Sanity checks
+	for n := range targetSecrets {
+		_, ok := sourceSecrets[n]
+		if !ok {
+			return fmt.Errorf("Migration target expects the \"%s\" socket but source isn't providing it", n)
+		}
+	}
+
+	if targetSecrets["control"] == "" {
+		return fmt.Errorf("Migration target didn't setup the required \"control\" socket")
+	}
+
+	// Struct used to hold everything together
+	type proxy struct {
+		done       chan bool
+		sourceConn *websocket.Conn
+		targetConn *websocket.Conn
+	}
+
+	proxies := map[string]*proxy{}
+
+	// Connect the control socket
+	sourceConn, err := source.GetOperationWebsocket(sourceOp.ID, sourceSecrets["control"])
+	if err != nil {
+		return err
+	}
+
+	targetConn, err := r.GetOperationWebsocket(targetOp.ID, targetSecrets["control"])
+	if err != nil {
+		return err
+	}
+
+	proxies["control"] = &proxy{
+		done:       shared.WebsocketProxy(sourceConn, targetConn),
+		sourceConn: sourceConn,
+		targetConn: targetConn,
+	}
+
+	// Connect the data sockets
+	for name := range sourceSecrets {
+		if name == "control" {
+			continue
+		}
+
+		// Handle resets (used for multiple objects)
+		sourceConn, err := source.GetOperationWebsocket(sourceOp.ID, sourceSecrets[name])
+		if err != nil {
+			break
+		}
+
+		targetConn, err := r.GetOperationWebsocket(targetOp.ID, targetSecrets[name])
+		if err != nil {
+			break
+		}
+
+		proxies[name] = &proxy{
+			sourceConn: sourceConn,
+			targetConn: targetConn,
+			done:       shared.WebsocketProxy(sourceConn, targetConn),
+		}
+	}
+
+	// Cleanup once everything is done
+	go func() {
+		// Wait for control socket
+		<-proxies["control"].done
+		proxies["control"].sourceConn.Close()
+		proxies["control"].targetConn.Close()
+
+		// Then deal with the others
+		for name, proxy := range proxies {
+			if name == "control" {
+				continue
+			}
+
+			<-proxy.done
+			proxy.sourceConn.Close()
+			proxy.targetConn.Close()
+		}
+	}()
+
+	return nil
+}
+
+// UpdateInstance updates the instance definition.
+func (r *ProtocolLXD) UpdateInstance(name string, instance api.InstancePut, ETag string) (Operation, error) {
+	// Send the request
+	op, _, err := r.queryOperation("PUT", fmt.Sprintf("/instances/%s", url.PathEscape(name)), instance, ETag)
+	if err != nil {
+		return nil, err
+	}
+
+	return op, nil
+}
+
+// RenameInstance requests that LXD renames the instance.
+func (r *ProtocolLXD) RenameInstance(name string, instance api.InstancePost) (Operation, error) {
+	// Sanity check
+	if instance.Migration {
+		return nil, fmt.Errorf("Can't ask for a migration through RenameInstance")
+	}
+
+	// Send the request
+	op, _, err := r.queryOperation("POST", fmt.Sprintf("/instances/%s", url.PathEscape(name)), instance, "")
+	if err != nil {
+		return nil, err
+	}
+
+	return op, nil
+}
+
+func (r *ProtocolLXD) tryMigrateInstance(source InstanceServer, name string, req api.InstancePost, urls []string) (RemoteOperation, error) {
+	if len(urls) == 0 {
+		return nil, fmt.Errorf("The target server isn't listening on the network")
+	}
+
+	rop := remoteOperation{
+		chDone: make(chan bool),
+	}
+
+	operation := req.Target.Operation
+
+	// Forward targetOp to remote op
+	go func() {
+		success := false
+		errors := map[string]error{}
+		for _, serverURL := range urls {
+			req.Target.Operation = fmt.Sprintf("%s/1.0/operations/%s", serverURL, url.PathEscape(operation))
+
+			op, err := source.MigrateInstance(name, req)
+			if err != nil {
+				errors[serverURL] = err
+				continue
+			}
+
+			rop.targetOp = op
+
+			for _, handler := range rop.handlers {
+				rop.targetOp.AddHandler(handler)
+			}
+
+			err = rop.targetOp.Wait()
+			if err != nil {
+				errors[serverURL] = err
+				continue
+			}
+
+			success = true
+			break
+		}
+
+		if !success {
+			rop.err = remoteOperationError("Failed instance migration", errors)
+		}
+
+		close(rop.chDone)
+	}()
+
+	return &rop, nil
+}
+
+// MigrateInstance requests that LXD prepares for a instance migration.
+func (r *ProtocolLXD) MigrateInstance(name string, instance api.InstancePost) (Operation, error) {
+	if instance.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 !instance.Migration {
+		return nil, fmt.Errorf("Can't ask for a rename through MigrateInstance")
+	}
+
+	// Send the request
+	op, _, err := r.queryOperation("POST", fmt.Sprintf("/instances/%s", url.PathEscape(name)), instance, "")
+	if err != nil {
+		return nil, err
+	}
+
+	return op, nil
+}
+
+// DeleteInstance requests that LXD deletes the instance.
+func (r *ProtocolLXD) DeleteInstance(name string) (Operation, error) {
+	// Send the request
+	op, _, err := r.queryOperation("DELETE", fmt.Sprintf("/instances/%s", url.PathEscape(name)), nil, "")
+	if err != nil {
+		return nil, err
+	}
+
+	return op, nil
+}
+
+// ExecInstance requests that LXD spawns a command inside the instance.
+func (r *ProtocolLXD) ExecInstance(instanceName string, exec api.InstanceExecPost, args *InstanceExecArgs) (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")
+		}
+	}
+
+	if exec.User > 0 || exec.Group > 0 || exec.Cwd != "" {
+		if !r.HasExtension("container_exec_user_group_cwd") {
+			return nil, fmt.Errorf("The server is missing the required \"container_exec_user_group_cwd\" API extension")
+		}
+	}
+
+	// Send the request
+	op, _, err := r.queryOperation("POST", fmt.Sprintf("/instances/%s/exec", url.PathEscape(instanceName)), exec, "")
+	if err != nil {
+		return nil, err
+	}
+	opAPI := op.Get()
+
+	// Process additional arguments
+	if args != nil {
+		// Parse the fds
+		fds := map[string]string{}
+
+		value, ok := opAPI.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(opAPI.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(opAPI.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()
+
+					if args.DataDone != nil {
+						close(args.DataDone)
+					}
+				}()
+			} else {
+				if args.DataDone != nil {
+					close(args.DataDone)
+				}
+			}
+		} else {
+			// Handle non-interactive sessions
+			dones := map[int]chan bool{}
+			conns := []*websocket.Conn{}
+
+			// Handle stdin
+			if fds["0"] != "" {
+				conn, err := r.GetOperationWebsocket(opAPI.ID, fds["0"])
+				if err != nil {
+					return nil, err
+				}
+
+				conns = append(conns, conn)
+				dones[0] = shared.WebsocketSendStream(conn, args.Stdin, -1)
+			}
+
+			// Handle stdout
+			if fds["1"] != "" {
+				conn, err := r.GetOperationWebsocket(opAPI.ID, fds["1"])
+				if err != nil {
+					return nil, err
+				}
+
+				conns = append(conns, conn)
+				dones[1] = shared.WebsocketRecvStream(args.Stdout, conn)
+			}
+
+			// Handle stderr
+			if fds["2"] != "" {
+				conn, err := r.GetOperationWebsocket(opAPI.ID, fds["2"])
+				if err != nil {
+					return nil, err
+				}
+
+				conns = append(conns, conn)
+				dones[2] = shared.WebsocketRecvStream(args.Stderr, conn)
+			}
+
+			// Wait for everything to be done
+			go func() {
+				for i, chDone := range dones {
+					// Skip stdin, dealing with it separately below
+					if i == 0 {
+						continue
+					}
+
+					<-chDone
+				}
+
+				if fds["0"] != "" {
+					if args.Stdin != nil {
+						args.Stdin.Close()
+					}
+
+					// Empty the stdin channel but don't block on it as
+					// stdin may be stuck in Read()
+					go func() {
+						<-dones[0]
+					}()
+				}
+
+				for _, conn := range conns {
+					conn.Close()
+				}
+
+				if args.DataDone != nil {
+					close(args.DataDone)
+				}
+			}()
+		}
+	}
+
+	return op, nil
+}
+
+// GetInstanceFile retrieves the provided path from the instance.
+func (r *ProtocolLXD) GetInstanceFile(instanceName string, path string) (io.ReadCloser, *InstanceFileResponse, error) {
+	// Prepare the HTTP request
+	requestURL, err := shared.URLEncode(
+		fmt.Sprintf("%s/1.0/instances/%s/files", r.httpHost, url.PathEscape(instanceName)),
+		map[string]string{"path": path})
+	if err != nil {
+		return nil, nil, err
+	}
+
+	requestURL, err = r.setQueryAttributes(requestURL)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	req, err := http.NewRequest("GET", requestURL, 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.do(req)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// Check the return value for a cleaner error
+	if resp.StatusCode != http.StatusOK {
+		_, _, err := lxdParseResponse(resp)
+		if err != nil {
+			return nil, nil, err
+		}
+	}
+
+	// Parse the headers
+	uid, gid, mode, fileType, _ := shared.ParseLXDFileHeaders(resp.Header)
+	fileResp := InstanceFileResponse{
+		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
+}
+
+// CreateInstanceFile tells LXD to create a file in the instance.
+func (r *ProtocolLXD) CreateInstanceFile(instanceName string, path string, args InstanceFileArgs) 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.Type == "symlink" {
+		if !r.HasExtension("file_symlinks") {
+			return fmt.Errorf("The server is missing the required \"file_symlinks\" 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
+	requestURL := fmt.Sprintf("%s/1.0/instances/%s/files?path=%s", r.httpHost, url.PathEscape(instanceName), url.QueryEscape(path))
+
+	requestURL, err := r.setQueryAttributes(requestURL)
+	if err != nil {
+		return err
+	}
+
+	req, err := http.NewRequest("POST", requestURL, 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
+	if args.UID > -1 {
+		req.Header.Set("X-LXD-uid", fmt.Sprintf("%d", args.UID))
+	}
+
+	if args.GID > -1 {
+		req.Header.Set("X-LXD-gid", fmt.Sprintf("%d", args.GID))
+	}
+
+	if args.Mode > -1 {
+		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.do(req)
+	if err != nil {
+		return err
+	}
+
+	// Check the return value for a cleaner error
+	_, _, err = lxdParseResponse(resp)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// DeleteInstanceFile deletes a file in the instance.
+func (r *ProtocolLXD) DeleteInstanceFile(instanceName 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("/instances/%s/files?path=%s", url.PathEscape(instanceName), url.QueryEscape(path)), nil, "")
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// GetInstanceSnapshotNames returns a list of snapshot names for the instance.
+func (r *ProtocolLXD) GetInstanceSnapshotNames(instanceName string) ([]string, error) {
+	urls := []string{}
+
+	// Fetch the raw value
+	_, err := r.queryStruct("GET", fmt.Sprintf("/instances/%s/snapshots", url.PathEscape(instanceName)), nil, "", &urls)
+	if err != nil {
+		return nil, err
+	}
+
+	// Parse it
+	names := []string{}
+	for _, uri := range urls {
+		fields := strings.Split(uri, fmt.Sprintf("/instances/%s/snapshots/", url.PathEscape(instanceName)))
+		names = append(names, fields[len(fields)-1])
+	}
+
+	return names, nil
+}
+
+// GetInstanceSnapshots returns a list of snapshots for the instance.
+func (r *ProtocolLXD) GetInstanceSnapshots(instanceName string) ([]api.InstanceSnapshot, error) {
+	snapshots := []api.InstanceSnapshot{}
+
+	// Fetch the raw value
+	_, err := r.queryStruct("GET", fmt.Sprintf("/instances/%s/snapshots?recursion=1", url.PathEscape(instanceName)), nil, "", &snapshots)
+	if err != nil {
+		return nil, err
+	}
+
+	return snapshots, nil
+}
+
+// GetInstanceSnapshot returns a Snapshot struct for the provided instance and snapshot names
+func (r *ProtocolLXD) GetInstanceSnapshot(instanceName string, name string) (*api.InstanceSnapshot, string, error) {
+	snapshot := api.InstanceSnapshot{}
+
+	// Fetch the raw value
+	etag, err := r.queryStruct("GET", fmt.Sprintf("/instances/%s/snapshots/%s", url.PathEscape(instanceName), url.PathEscape(name)), nil, "", &snapshot)
+	if err != nil {
+		return nil, "", err
+	}
+
+	return &snapshot, etag, nil
+}
+
+// CreateInstanceSnapshot requests that LXD creates a new snapshot for the instance.
+func (r *ProtocolLXD) CreateInstanceSnapshot(instanceName string, snapshot api.InstanceSnapshotsPost) (Operation, error) {
+	// Validate the request
+	if snapshot.ExpiresAt != nil && !r.HasExtension("snapshot_expiry_creation") {
+		return nil, fmt.Errorf("The server is missing the required \"snapshot_expiry_creation\" API extension")
+	}
+
+	// Send the request
+	op, _, err := r.queryOperation("POST", fmt.Sprintf("/instances/%s/snapshots", url.PathEscape(instanceName)), snapshot, "")
+	if err != nil {
+		return nil, err
+	}
+
+	return op, nil
+}
+
+// CopyInstanceSnapshot copies a snapshot from a remote server into a new instance. Additional options can be passed using InstanceCopyArgs.
+func (r *ProtocolLXD) CopyInstanceSnapshot(source InstanceServer, instanceName string, snapshot api.InstanceSnapshot, args *InstanceSnapshotCopyArgs) (RemoteOperation, error) {
+	// Backward compatibility (with broken Name field)
+	fields := strings.Split(snapshot.Name, shared.SnapshotDelimiter)
+	cName := instanceName
+	sName := fields[len(fields)-1]
+
+	// Base request
+	req := api.InstancesPost{
+		Name: cName,
+		InstancePut: api.InstancePut{
+			Architecture: snapshot.Architecture,
+			Config:       snapshot.Config,
+			Devices:      snapshot.Devices,
+			Ephemeral:    snapshot.Ephemeral,
+			Profiles:     snapshot.Profiles,
+		},
+	}
+
+	if snapshot.Stateful && args.Live {
+		if !r.HasExtension("container_snapshot_stateful_migration") {
+			return nil, fmt.Errorf("The server is missing the required \"container_snapshot_stateful_migration\" API extension")
+		}
+		req.InstancePut.Stateful = snapshot.Stateful
+		req.Source.Live = args.Live
+	}
+	req.Source.BaseImage = snapshot.Config["volatile.base_image"]
+
+	// Process the copy arguments
+	if args != nil {
+		// Sanity checks
+		if shared.StringInSlice(args.Mode, []string{"push", "relay"}) {
+			if !r.HasExtension("container_push") {
+				return nil, fmt.Errorf("The target server is missing the required \"container_push\" API extension")
+			}
+
+			if !source.HasExtension("container_push") {
+				return nil, fmt.Errorf("The source server is missing the required \"container_push\" API extension")
+			}
+		}
+
+		if args.Mode == "push" && !source.HasExtension("container_push_target") {
+			return nil, fmt.Errorf("The source server is missing the required \"container_push_target\" API extension")
+		}
+
+		// Allow overriding the target name
+		if args.Name != "" {
+			req.Name = args.Name
+		}
+	}
+
+	sourceInfo, err := source.GetConnectionInfo()
+	if err != nil {
+		return nil, fmt.Errorf("Failed to get source connection info: %v", err)
+	}
+
+	destInfo, err := r.GetConnectionInfo()
+	if err != nil {
+		return nil, fmt.Errorf("Failed to get destination connection info: %v", err)
+	}
+
+	instance, _, err := source.GetInstance(cName)
+	if err != nil {
+		return nil, fmt.Errorf("Failed to get instance info: %v", err)
+	}
+
+	// Optimization for the local copy case
+	if destInfo.URL == sourceInfo.URL && destInfo.SocketPath == sourceInfo.SocketPath && (!r.IsClustered() || instance.Location == r.clusterTarget || r.HasExtension("cluster_internal_copy")) {
+		// Project handling
+		if destInfo.Project != sourceInfo.Project {
+			if !r.HasExtension("container_copy_project") {
+				return nil, fmt.Errorf("The server is missing the required \"container_copy_project\" API extension")
+			}
+
+			req.Source.Project = sourceInfo.Project
+		}
+
+		// Local copy source fields
+		req.Source.Type = "copy"
+		req.Source.Source = fmt.Sprintf("%s/%s", cName, sName)
+
+		// Copy the instance
+		op, err := r.CreateInstance(req)
+		if err != nil {
+			return nil, err
+		}
+
+		rop := remoteOperation{
+			targetOp: op,
+			chDone:   make(chan bool),
+		}
+
+		// Forward targetOp to remote op
+		go func() {
+			rop.err = rop.targetOp.Wait()
+			close(rop.chDone)
+		}()
+
+		return &rop, nil
+	}
+
+	// Source request
+	sourceReq := api.InstanceSnapshotPost{
+		Migration: true,
+		Name:      args.Name,
+	}
+	if snapshot.Stateful && args.Live {
+		sourceReq.Live = args.Live
+	}
+
+	// Push mode migration
+	if args != nil && args.Mode == "push" {
+		// Get target server connection information
+		info, err := r.GetConnectionInfo()
+		if err != nil {
+			return nil, err
+		}
+
+		// Create the instance
+		req.Source.Type = "migration"
+		req.Source.Mode = "push"
+
+		op, err := r.CreateInstance(req)
+		if err != nil {
+			return nil, err
+		}
+		opAPI := op.Get()
+
+		targetSecrets := map[string]string{}
+		for k, v := range opAPI.Metadata {
+			targetSecrets[k] = v.(string)
+		}
+
+		// Prepare the source request
+		target := api.InstancePostTarget{}
+		target.Operation = opAPI.ID
+		target.Websockets = targetSecrets
+		target.Certificate = info.Certificate
+		sourceReq.Target = &target
+
+		return r.tryMigrateInstanceSnapshot(source, cName, sName, sourceReq, info.Addresses)
+	}
+
+	// Get source server connection information
+	info, err := source.GetConnectionInfo()
+	if err != nil {
+		return nil, err
+	}
+
+	op, err := source.MigrateInstanceSnapshot(cName, sName, sourceReq)
+	if err != nil {
+		return nil, err
+	}
+	opAPI := op.Get()
+
+	sourceSecrets := map[string]string{}
+	for k, v := range opAPI.Metadata {
+		sourceSecrets[k] = v.(string)
+	}
+
+	// Relay mode migration
+	if args != nil && args.Mode == "relay" {
+		// Push copy source fields
+		req.Source.Type = "migration"
+		req.Source.Mode = "push"
+
+		// Start the process
+		targetOp, err := r.CreateInstance(req)
+		if err != nil {
+			return nil, err
+		}
+		targetOpAPI := targetOp.Get()
+
+		// Extract the websockets
+		targetSecrets := map[string]string{}
+		for k, v := range targetOpAPI.Metadata {
+			targetSecrets[k] = v.(string)
+		}
+
+		// Launch the relay
+		err = r.proxyMigration(targetOp.(*operation), targetSecrets, source, op.(*operation), sourceSecrets)
+		if err != nil {
+			return nil, err
+		}
+
+		// Prepare a tracking operation
+		rop := remoteOperation{
+			targetOp: targetOp,
+			chDone:   make(chan bool),
+		}
+
+		// Forward targetOp to remote op
+		go func() {
+			rop.err = rop.targetOp.Wait()
+			close(rop.chDone)
+		}()
+
+		return &rop, nil
+	}
+
+	// Pull mode migration
+	req.Source.Type = "migration"
+	req.Source.Mode = "pull"
+	req.Source.Operation = opAPI.ID
+	req.Source.Websockets = sourceSecrets
+	req.Source.Certificate = info.Certificate
+
+	return r.tryCreateInstance(req, info.Addresses)
+}
+
+// RenameInstanceSnapshot requests that LXD renames the snapshot.
+func (r *ProtocolLXD) RenameInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPost) (Operation, error) {
+	// Sanity check
+	if instance.Migration {
+		return nil, fmt.Errorf("Can't ask for a migration through RenameInstanceSnapshot")
+	}
+
+	// Send the request
+	op, _, err := r.queryOperation("POST", fmt.Sprintf("/instances/%s/snapshots/%s", url.PathEscape(instanceName), url.PathEscape(name)), instance, "")
+	if err != nil {
+		return nil, err
+	}
+
+	return op, nil
+}
+
+func (r *ProtocolLXD) tryMigrateInstanceSnapshot(source InstanceServer, instanceName string, name string, req api.InstanceSnapshotPost, urls []string) (RemoteOperation, error) {
+	if len(urls) == 0 {
+		return nil, fmt.Errorf("The target server isn't listening on the network")
+	}
+
+	rop := remoteOperation{
+		chDone: make(chan bool),
+	}
+
+	operation := req.Target.Operation
+
+	// Forward targetOp to remote op
+	go func() {
+		success := false
+		errors := map[string]error{}
+		for _, serverURL := range urls {
+			req.Target.Operation = fmt.Sprintf("%s/1.0/operations/%s", serverURL, url.PathEscape(operation))
+
+			op, err := source.MigrateInstanceSnapshot(instanceName, name, req)
+			if err != nil {
+				errors[serverURL] = err
+				continue
+			}
+
+			rop.targetOp = op
+
+			for _, handler := range rop.handlers {
+				rop.targetOp.AddHandler(handler)
+			}
+
+			err = rop.targetOp.Wait()
+			if err != nil {
+				errors[serverURL] = err
+				continue
+			}
+
+			success = true
+			break
+		}
+
+		if !success {
+			rop.err = remoteOperationError("Failed instance migration", errors)
+		}
+
+		close(rop.chDone)
+	}()
+
+	return &rop, nil
+}
+
+// MigrateInstanceSnapshot requests that LXD prepares for a snapshot migration.
+func (r *ProtocolLXD) MigrateInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPost) (Operation, error) {
+	// Sanity check
+	if !instance.Migration {
+		return nil, fmt.Errorf("Can't ask for a rename through MigrateInstanceSnapshot")
+	}
+
+	// Send the request
+	op, _, err := r.queryOperation("POST", fmt.Sprintf("/instances/%s/snapshots/%s", url.PathEscape(instanceName), url.PathEscape(name)), instance, "")
+	if err != nil {
+		return nil, err
+	}
+
+	return op, nil
+}
+
+// DeleteInstanceSnapshot requests that LXD deletes the instance snapshot.
+func (r *ProtocolLXD) DeleteInstanceSnapshot(instanceName string, name string) (Operation, error) {
+	// Send the request
+	op, _, err := r.queryOperation("DELETE", fmt.Sprintf("/instances/%s/snapshots/%s", url.PathEscape(instanceName), url.PathEscape(name)), nil, "")
+	if err != nil {
+		return nil, err
+	}
+
+	return op, nil
+}
+
+// UpdateInstanceSnapshot requests that LXD updates the instance snapshot.
+func (r *ProtocolLXD) UpdateInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPut, ETag string) (Operation, error) {
+	if !r.HasExtension("snapshot_expiry") {
+		return nil, fmt.Errorf("The server is missing the required \"snapshot_expiry\" API extension")
+	}
+
+	// Send the request
+	op, _, err := r.queryOperation("PUT", fmt.Sprintf("/instances/%s/snapshots/%s",
+		url.PathEscape(instanceName), url.PathEscape(name)), instance, ETag)
+	if err != nil {
+		return nil, err
+	}
+
+	return op, nil
+}
+
+// GetInstanceState returns a InstanceState entry for the provided instance name.
+func (r *ProtocolLXD) GetInstanceState(name string) (*api.InstanceState, string, error) {
+	state := api.InstanceState{}
+
+	// Fetch the raw value
+	etag, err := r.queryStruct("GET", fmt.Sprintf("/instances/%s/state", url.PathEscape(name)), nil, "", &state)
+	if err != nil {
+		return nil, "", err
+	}
+
+	return &state, etag, nil
+}
+
+// UpdateInstanceState updates the instance to match the requested state.
+func (r *ProtocolLXD) UpdateInstanceState(name string, state api.InstanceStatePut, ETag string) (Operation, error) {
+	// Send the request
+	op, _, err := r.queryOperation("PUT", fmt.Sprintf("/instances/%s/state", url.PathEscape(name)), state, ETag)
+	if err != nil {
+		return nil, err
+	}
+
+	return op, nil
+}
+
+// GetInstanceLogfiles returns a list of logfiles for the instance.
+func (r *ProtocolLXD) GetInstanceLogfiles(name string) ([]string, error) {
+	urls := []string{}
+
+	// Fetch the raw value
+	_, err := r.queryStruct("GET", fmt.Sprintf("/instances/%s/logs", url.PathEscape(name)), nil, "", &urls)
+	if err != nil {
+		return nil, err
+	}
+
+	// Parse it
+	logfiles := []string{}
+	for _, uri := range logfiles {
+		fields := strings.Split(uri, fmt.Sprintf("/instances/%s/logs/", url.PathEscape(name)))
+		logfiles = append(logfiles, fields[len(fields)-1])
+	}
+
+	return logfiles, nil
+}
+
+// GetInstanceLogfile returns the content of the requested logfile.
+//
+// Note that it's the caller's responsibility to close the returned ReadCloser
+func (r *ProtocolLXD) GetInstanceLogfile(name string, filename string) (io.ReadCloser, error) {
+	// Prepare the HTTP request
+	url := fmt.Sprintf("%s/1.0/instances/%s/logs/%s", r.httpHost, url.PathEscape(name), url.PathEscape(filename))
+
+	url, err := r.setQueryAttributes(url)
+	if err != nil {
+		return nil, err
+	}
+
+	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.do(req)
+	if err != nil {
+		return nil, err
+	}
+
+	// Check the return value for a cleaner error
+	if resp.StatusCode != http.StatusOK {
+		_, _, err := lxdParseResponse(resp)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return resp.Body, err
+}
+
+// DeleteInstanceLogfile deletes the requested logfile.
+func (r *ProtocolLXD) DeleteInstanceLogfile(name string, filename string) error {
+	// Send the request
+	_, _, err := r.query("DELETE", fmt.Sprintf("/instances/%s/logs/%s", url.PathEscape(name), url.PathEscape(filename)), nil, "")
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// GetInstanceMetadata returns instance metadata.
+func (r *ProtocolLXD) GetInstanceMetadata(name string) (*api.ImageMetadata, string, error) {
+	if !r.HasExtension("container_edit_metadata") {
+		return nil, "", fmt.Errorf("The server is missing the required \"container_edit_metadata\" API extension")
+	}
+
+	metadata := api.ImageMetadata{}
+
+	url := fmt.Sprintf("/instances/%s/metadata", url.PathEscape(name))
+	etag, err := r.queryStruct("GET", url, nil, "", &metadata)
+	if err != nil {
+		return nil, "", err
+	}
+
+	return &metadata, etag, err
+}
+
+// SetInstanceMetadata sets the content of the instance metadata file.
+func (r *ProtocolLXD) SetInstanceMetadata(name string, metadata api.ImageMetadata, ETag string) error {
+	if !r.HasExtension("container_edit_metadata") {
+		return fmt.Errorf("The server is missing the required \"container_edit_metadata\" API extension")
+	}
+
+	url := fmt.Sprintf("/instances/%s/metadata", url.PathEscape(name))
+	_, _, err := r.query("PUT", url, metadata, ETag)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// GetInstanceTemplateFiles returns the list of names of template files for a instance.
+func (r *ProtocolLXD) GetInstanceTemplateFiles(instanceName string) ([]string, error) {
+	if !r.HasExtension("container_edit_metadata") {
+		return nil, fmt.Errorf("The server is missing the required \"container_edit_metadata\" API extension")
+	}
+
+	templates := []string{}
+
+	url := fmt.Sprintf("/instances/%s/metadata/templates", url.PathEscape(instanceName))
+	_, err := r.queryStruct("GET", url, nil, "", &templates)
+	if err != nil {
+		return nil, err
+	}
+
+	return templates, nil
+}
+
+// GetInstanceTemplateFile returns the content of a template file for a instance.
+func (r *ProtocolLXD) GetInstanceTemplateFile(instanceName string, templateName string) (io.ReadCloser, error) {
+	if !r.HasExtension("container_edit_metadata") {
+		return nil, fmt.Errorf("The server is missing the required \"container_edit_metadata\" API extension")
+	}
+
+	url := fmt.Sprintf("%s/1.0/instances/%s/metadata/templates?path=%s", r.httpHost, url.PathEscape(instanceName), url.QueryEscape(templateName))
+
+	url, err := r.setQueryAttributes(url)
+	if err != nil {
+		return nil, err
+	}
+
+	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.do(req)
+	if err != nil {
+		return nil, err
+	}
+
+	// Check the return value for a cleaner error
+	if resp.StatusCode != http.StatusOK {
+		_, _, err := lxdParseResponse(resp)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return resp.Body, err
+}
+
+// CreateInstanceTemplateFile creates an a template for a instance.
+func (r *ProtocolLXD) CreateInstanceTemplateFile(instanceName string, templateName string, content io.ReadSeeker) error {
+	return r.setInstanceTemplateFile(instanceName, templateName, content, "POST")
+}
+
+// UpdateInstanceTemplateFile updates the content for a instance template file.
+func (r *ProtocolLXD) UpdateInstanceTemplateFile(instanceName string, templateName string, content io.ReadSeeker) error {
+	return r.setInstanceTemplateFile(instanceName, templateName, content, "PUT")
+}
+
+func (r *ProtocolLXD) setInstanceTemplateFile(instanceName string, templateName string, content io.ReadSeeker, httpMethod string) error {
+	if !r.HasExtension("container_edit_metadata") {
+		return fmt.Errorf("The server is missing the required \"container_edit_metadata\" API extension")
+	}
+
+	url := fmt.Sprintf("%s/1.0/instances/%s/metadata/templates?path=%s", r.httpHost, url.PathEscape(instanceName), url.QueryEscape(templateName))
+
+	url, err := r.setQueryAttributes(url)
+	if err != nil {
+		return err
+	}
+
+	req, err := http.NewRequest(httpMethod, url, content)
+	if err != nil {
+		return err
+	}
+	req.Header.Set("Content-Type", "application/octet-stream")
+
+	// Set the user agent
+	if r.httpUserAgent != "" {
+		req.Header.Set("User-Agent", r.httpUserAgent)
+	}
+
+	// Send the request
+	resp, err := r.do(req)
+	// Check the return value for a cleaner error
+	if resp.StatusCode != http.StatusOK {
+		_, _, err := lxdParseResponse(resp)
+		if err != nil {
+			return err
+		}
+	}
+	return err
+}
+
+// DeleteInstanceTemplateFile deletes a template file for a instance.
+func (r *ProtocolLXD) DeleteInstanceTemplateFile(name string, templateName string) error {
+	if !r.HasExtension("container_edit_metadata") {
+		return fmt.Errorf("The server is missing the required \"container_edit_metadata\" API extension")
+	}
+	_, _, err := r.query("DELETE", fmt.Sprintf("/instances/%s/metadata/templates?path=%s", url.PathEscape(name), url.QueryEscape(templateName)), nil, "")
+	return err
+}
+
+// ConsoleInstance requests that LXD attaches to the console device of a instance.
+func (r *ProtocolLXD) ConsoleInstance(instanceName string, console api.InstanceConsolePost, args *InstanceConsoleArgs) (Operation, error) {
+	if !r.HasExtension("console") {
+		return nil, fmt.Errorf("The server is missing the required \"console\" API extension")
+	}
+
+	// Send the request
+	op, _, err := r.queryOperation("POST", fmt.Sprintf("/instances/%s/console", url.PathEscape(instanceName)), console, "")
+	if err != nil {
+		return nil, err
+	}
+	opAPI := op.Get()
+
+	if args == nil || args.Terminal == nil {
+		return nil, fmt.Errorf("A terminal must be set")
+	}
+
+	if args.Control == nil {
+		return nil, fmt.Errorf("A control channel must be set")
+	}
+
+	// Parse the fds
+	fds := map[string]string{}
+
+	value, ok := opAPI.Metadata["fds"]
+	if ok {
+		values := value.(map[string]interface{})
+		for k, v := range values {
+			fds[k] = v.(string)
+		}
+	}
+
+	var controlConn *websocket.Conn
+	// Call the control handler with a connection to the control socket
+	if fds["control"] == "" {
+		return nil, fmt.Errorf("Did not receive a file descriptor for the control channel")
+	}
+
+	controlConn, err = r.GetOperationWebsocket(opAPI.ID, fds["control"])
+	if err != nil {
+		return nil, err
+	}
+
+	go args.Control(controlConn)
+
+	// Connect to the websocket
+	conn, err := r.GetOperationWebsocket(opAPI.ID, fds["0"])
+	if err != nil {
+		return nil, err
+	}
+
+	// Detach from console.
+	go func(consoleDisconnect <-chan bool) {
+		<-consoleDisconnect
+		msg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "Detaching from console")
+		// We don't care if this fails. This is just for convenience.
+		controlConn.WriteMessage(websocket.CloseMessage, msg)
+		controlConn.Close()
+	}(args.ConsoleDisconnect)
+
+	// And attach stdin and stdout to it
+	go func() {
+		shared.WebsocketSendStream(conn, args.Terminal, -1)
+		<-shared.WebsocketRecvStream(args.Terminal, conn)
+		conn.Close()
+	}()
+
+	return op, nil
+}
+
+// GetInstanceConsoleLog requests that LXD attaches to the console device of a instance.
+//
+// Note that it's the caller's responsibility to close the returned ReadCloser
+func (r *ProtocolLXD) GetInstanceConsoleLog(instanceName string, args *InstanceConsoleLogArgs) (io.ReadCloser, error) {
+	if !r.HasExtension("console") {
+		return nil, fmt.Errorf("The server is missing the required \"console\" API extension")
+	}
+
+	// Prepare the HTTP request
+	url := fmt.Sprintf("%s/1.0/instances/%s/console", r.httpHost, url.PathEscape(instanceName))
+
+	url, err := r.setQueryAttributes(url)
+	if err != nil {
+		return nil, err
+	}
+
+	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.do(req)
+	if err != nil {
+		return nil, err
+	}
+
+	// Check the return value for a cleaner error
+	if resp.StatusCode != http.StatusOK {
+		_, _, err := lxdParseResponse(resp)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return resp.Body, err
+}
+
+// DeleteInstanceConsoleLog deletes the requested instance's console log.
+func (r *ProtocolLXD) DeleteInstanceConsoleLog(instanceName string, args *InstanceConsoleLogArgs) error {
+	if !r.HasExtension("console") {
+		return fmt.Errorf("The server is missing the required \"console\" API extension")
+	}
+
+	// Send the request
+	_, _, err := r.query("DELETE", fmt.Sprintf("/instances/%s/console", url.PathEscape(instanceName)), nil, "")
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// GetInstanceBackupNames returns a list of backup names for the instance.
+func (r *ProtocolLXD) GetInstanceBackupNames(instanceName string) ([]string, error) {
+	if !r.HasExtension("container_backup") {
+		return nil, fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+	}
+
+	// Fetch the raw value
+	urls := []string{}
+	_, err := r.queryStruct("GET", fmt.Sprintf("/instances/%s/backups",
+		url.PathEscape(instanceName)), nil, "", &urls)
+	if err != nil {
+		return nil, err
+	}
+
+	// Parse it
+	names := []string{}
+	for _, uri := range urls {
+		fields := strings.Split(uri, fmt.Sprintf("/instances/%s/backups/",
+			url.PathEscape(instanceName)))
+		names = append(names, fields[len(fields)-1])
+	}
+
+	return names, nil
+}
+
+// GetInstanceBackups returns a list of backups for the instance.
+func (r *ProtocolLXD) GetInstanceBackups(instanceName string) ([]api.InstanceBackup, error) {
+	if !r.HasExtension("container_backup") {
+		return nil, fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+	}
+
+	// Fetch the raw value
+	backups := []api.InstanceBackup{}
+
+	_, err := r.queryStruct("GET", fmt.Sprintf("/instances/%s/backups?recursion=1", url.PathEscape(instanceName)), nil, "", &backups)
+	if err != nil {
+		return nil, err
+	}
+
+	return backups, nil
+}
+
+// GetInstanceBackup returns a Backup struct for the provided instance and backup names.
+func (r *ProtocolLXD) GetInstanceBackup(instanceName string, name string) (*api.InstanceBackup, string, error) {
+	if !r.HasExtension("container_backup") {
+		return nil, "", fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+	}
+
+	// Fetch the raw value
+	backup := api.InstanceBackup{}
+	etag, err := r.queryStruct("GET", fmt.Sprintf("/instances/%s/backups/%s", url.PathEscape(instanceName), url.PathEscape(name)), nil, "", &backup)
+	if err != nil {
+		return nil, "", err
+	}
+
+	return &backup, etag, nil
+}
+
+// CreateInstanceBackup requests that LXD creates a new backup for the instance.
+func (r *ProtocolLXD) CreateInstanceBackup(instanceName string, backup api.InstanceBackupsPost) (Operation, error) {
+	if !r.HasExtension("container_backup") {
+		return nil, fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+	}
+
+	// Send the request
+	op, _, err := r.queryOperation("POST", fmt.Sprintf("/instances/%s/backups",
+		url.PathEscape(instanceName)), backup, "")
+	if err != nil {
+		return nil, err
+	}
+
+	return op, nil
+}
+
+// RenameInstanceBackup requests that LXD renames the backup.
+func (r *ProtocolLXD) RenameInstanceBackup(instanceName string, name string, backup api.InstanceBackupPost) (Operation, error) {
+	if !r.HasExtension("container_backup") {
+		return nil, fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+	}
+
+	// Send the request
+	op, _, err := r.queryOperation("POST", fmt.Sprintf("/instances/%s/backups/%s",
+		url.PathEscape(instanceName), url.PathEscape(name)), backup, "")
+	if err != nil {
+		return nil, err
+	}
+
+	return op, nil
+}
+
+// DeleteInstanceBackup requests that LXD deletes the instance backup.
+func (r *ProtocolLXD) DeleteInstanceBackup(instanceName string, name string) (Operation, error) {
+	if !r.HasExtension("container_backup") {
+		return nil, fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+	}
+
+	// Send the request
+	op, _, err := r.queryOperation("DELETE", fmt.Sprintf("/instances/%s/backups/%s",
+		url.PathEscape(instanceName), url.PathEscape(name)), nil, "")
+	if err != nil {
+		return nil, err
+	}
+
+	return op, nil
+}
+
+// GetInstanceBackupFile requests the instance backup content.
+func (r *ProtocolLXD) GetInstanceBackupFile(instanceName string, name string, req *BackupFileRequest) (*BackupFileResponse, error) {
+	if !r.HasExtension("container_backup") {
+		return nil, fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+	}
+
+	// Build the URL
+	uri := fmt.Sprintf("%s/1.0/instances/%s/backups/%s/export", r.httpHost,
+		url.PathEscape(instanceName), url.PathEscape(name))
+	if r.project != "" {
+		uri += fmt.Sprintf("?project=%s", url.QueryEscape(r.project))
+	}
+
+	// Prepare the download request
+	request, err := http.NewRequest("GET", uri, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	if r.httpUserAgent != "" {
+		request.Header.Set("User-Agent", r.httpUserAgent)
+	}
+
+	// Start the request
+	response, doneCh, err := cancel.CancelableDownload(req.Canceler, r.http, request)
+	if err != nil {
+		return nil, err
+	}
+	defer response.Body.Close()
+	defer close(doneCh)
+
+	if response.StatusCode != http.StatusOK {
+		_, _, err := lxdParseResponse(response)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// 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(ioprogress.ProgressData{Text: fmt.Sprintf("%d%% (%s/s)", percent, units.GetByteSizeString(speed, 2))})
+				},
+			},
+		}
+	}
+
+	size, err := io.Copy(req.BackupFile, body)
+	if err != nil {
+		return nil, err
+	}
+
+	resp := BackupFileResponse{}
+	resp.Size = size
+
+	return &resp, nil
+}


More information about the lxc-devel mailing list