The following pull request was submitted through Github.
It can be accessed and reviewed at: https://github.com/lxc/lxd/pull/6197

This e-mail was sent by the LXC bot, direct replies will not reach the author
unless they happen to be subscribed to this list.

=== Description (from pull-request) ===

From c222666f66fd129e49a92f19dda1365a2c9392f0 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parr...@canonical.com>
Date: Thu, 12 Sep 2019 15:46:44 +0100
Subject: [PATCH 1/3] client/interfaces: Populates InstanceServer with rest of
 functions

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

diff --git a/client/interfaces.go b/client/interfaces.go
index 2fe2e1aadb..a7179830d3 100644
--- a/client/interfaces.go
+++ b/client/interfaces.go
@@ -136,6 +136,63 @@ type InstanceServer interface {
        UpdateContainerTemplateFile(containerName string, templateName string, 
content io.ReadSeeker) (err error)
        DeleteContainerTemplateFile(name string, templateName string) (err 
error)
 
+       // Instance functions.
+       GetInstanceNames(instanceType api.InstanceType) (names []string, err 
error)
+       GetInstances(instanceType api.InstanceType) (instances []api.Instance, 
err error)
+       GetInstancesFull(instanceType api.InstanceType) (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 
*ContainerCopyArgs) (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 
*ContainerExecArgs) (op Operation, err error)
+       ConsoleInstance(instanceName string, console api.InstanceConsolePost, 
args *ContainerConsoleArgs) (op Operation, err error)
+       GetInstanceConsoleLog(instanceName string, args 
*ContainerConsoleLogArgs) (content io.ReadCloser, err error)
+       DeleteInstanceConsoleLog(instanceName string, args 
*ContainerConsoleLogArgs) (err error)
+
+       GetInstanceFile(instanceName string, path string) (content 
io.ReadCloser, resp *ContainerFileResponse, err error)
+       CreateInstanceFile(instanceName string, path string, args 
ContainerFileArgs) (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 *ContainerSnapshotCopyArgs) (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 ContainerBackupArgs) (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)
+       DeleteInstanceTemplateFile(name string, templateName string) (err error)
+
        // Event handling functions
        GetEvents() (listener *EventListener, err error)
 

From 30b857bdf6717cf8d920818254624250cfd41d81 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parr...@canonical.com>
Date: Thu, 12 Sep 2019 15:50:34 +0100
Subject: [PATCH 2/3] client/lxd/instances: Adds instance related functions

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

Signed-off-by: Thomas Parrott <thomas.parr...@canonical.com>
---
 client/lxd_instances.go | 2154 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 2154 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..e2345814d8
--- /dev/null
+++ b/client/lxd_instances.go
@@ -0,0 +1,2154 @@
+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.
+
+// instanceTypeToPath converts the instance type to a URL path prefix and 
query string values.
+// If the remote server doesn't have the instances extension then the 
/containers endpoint is used
+// as long as the requested instanceType is any or container.
+func (r *ProtocolLXD) instanceTypeToPath(instanceType api.InstanceType) 
(string, url.Values, error) {
+       v := url.Values{}
+
+       // If the remote server doesn't support instances extension, check that 
only containers
+       // or any type has been requested and then fallback to using the old 
/containers endpoint.
+       if !r.HasExtension("instances") {
+               if instanceType == api.InstanceTypeContainer || instanceType == 
api.InstanceTypeAny {
+                       return "/containers", v, nil
+               }
+
+               return "", v, fmt.Errorf("Requested instance type not supported 
by server")
+       }
+
+       // If a specific instance type has been requested, add the 
instance-type filter parameter
+       // to the returned URL values so that it can be used in the final URL 
if needed to filter
+       // the result set being returned.
+       if instanceType != api.InstanceTypeAny {
+               v.Set("instance-type", string(instanceType))
+       }
+
+       return "/instances", v, nil
+}
+
+// GetInstanceNames returns a list of instance names.
+func (r *ProtocolLXD) GetInstanceNames(instanceType api.InstanceType) 
([]string, error) {
+       urls := []string{}
+
+       path, v, err := r.instanceTypeToPath(instanceType)
+       if err != nil {
+               return nil, err
+       }
+
+       // Fetch the raw value
+       _, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), 
nil, "", &urls)
+       if err != nil {
+               return nil, err
+       }
+
+       // Parse it
+       names := []string{}
+       prefix := path + "/"
+       for _, url := range urls {
+               fields := strings.Split(url, prefix)
+               names = append(names, fields[len(fields)-1])
+       }
+
+       return names, nil
+}
+
+// GetInstances returns a list of instances.
+func (r *ProtocolLXD) GetInstances(instanceType api.InstanceType) 
([]api.Instance, error) {
+       instances := []api.Instance{}
+
+       path, v, err := r.instanceTypeToPath(instanceType)
+       if err != nil {
+               return nil, err
+       }
+
+       v.Set("recursion", "1")
+
+       // Fetch the raw value
+       _, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), 
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(instanceType api.InstanceType) 
([]api.InstanceFull, error) {
+       instances := []api.InstanceFull{}
+
+       path, v, err := r.instanceTypeToPath(instanceType)
+       if err != nil {
+               return nil, err
+       }
+
+       v.Set("recursion", "2")
+
+       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", fmt.Sprintf("%s?%s", path, v.Encode()), 
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{}
+
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, "", err
+       }
+
+       // Fetch the raw value
+       etag, err := r.queryStruct("GET", fmt.Sprintf("%s/%s", path, 
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 ContainerBackupArgs) 
(Operation, error) {
+       if !r.HasExtension("container_backup") {
+               return nil, fmt.Errorf("The server is missing the required 
\"container_backup\" API extension")
+       }
+
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       if args.PoolName == "" {
+               // Send the request
+               op, _, err := r.queryOperation("POST", path, 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%s", r.httpHost, 
path))
+       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) {
+       path, _, err := r.instanceTypeToPath(instance.Type)
+       if err != nil {
+               return nil, err
+       }
+
+       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", path, 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 *ContainerCopyArgs) (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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       // Send the request
+       op, _, err := r.queryOperation("PUT", fmt.Sprintf("%s/%s", path, 
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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       // 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("%s/%s", path, 
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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       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("%s/%s", path, 
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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       // Send the request
+       op, _, err := r.queryOperation("DELETE", fmt.Sprintf("%s/%s", path, 
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 *ContainerExecArgs) (Operation, error) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       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("%s/%s/exec", path, 
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, *ContainerFileResponse, error) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, nil, err
+       }
+
+       // Prepare the HTTP request
+       requestURL, err := shared.URLEncode(
+               fmt.Sprintf("%s/1.0%s/%s/files", r.httpHost, path, 
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 := 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
+}
+
+// CreateInstanceFile tells LXD to create a file in the instance.
+func (r *ProtocolLXD) CreateInstanceFile(instanceName string, path string, 
args ContainerFileArgs) error {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return err
+       }
+
+       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%s/%s/files?path=%s", r.httpHost, 
path, 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 {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return err
+       }
+
+       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("%s/%s/files?path=%s", path, 
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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       urls := []string{}
+
+       // Fetch the raw value
+       _, err = r.queryStruct("GET", fmt.Sprintf("%s/%s/snapshots", path, 
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("%s/%s/snapshots/", 
path, 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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       snapshots := []api.InstanceSnapshot{}
+
+       // Fetch the raw value
+       _, err = r.queryStruct("GET", 
fmt.Sprintf("%s/%s/snapshots?recursion=1", path, 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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, "", err
+       }
+
+       snapshot := api.InstanceSnapshot{}
+
+       // Fetch the raw value
+       etag, err := r.queryStruct("GET", fmt.Sprintf("%s/%s/snapshots/%s", 
path, 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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       // 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("%s/%s/snapshots", 
path, 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 *ContainerSnapshotCopyArgs) 
(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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       // 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("%s/%s/snapshots/%s", path, 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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       // 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("%s/%s/snapshots/%s", path, 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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       // Send the request
+       op, _, err := r.queryOperation("DELETE", 
fmt.Sprintf("%s/%s/snapshots/%s", path, 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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       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("%s/%s/snapshots/%s", 
path, 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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, "", err
+       }
+
+       state := api.InstanceState{}
+
+       // Fetch the raw value
+       etag, err := r.queryStruct("GET", fmt.Sprintf("%s/%s/state", path, 
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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       // Send the request
+       op, _, err := r.queryOperation("PUT", fmt.Sprintf("%s/%s/state", path, 
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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       urls := []string{}
+
+       // Fetch the raw value
+       _, err = r.queryStruct("GET", fmt.Sprintf("%s/%s/logs", path, 
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("%s/%s/logs/", path, 
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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       // Prepare the HTTP request
+       url := fmt.Sprintf("%s/1.0%s/%s/logs/%s", r.httpHost, path, 
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 {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return err
+       }
+
+       // Send the request
+       _, _, err = r.query("DELETE", fmt.Sprintf("%s/%s/logs/%s", path, 
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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, "", err
+       }
+
+       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("%s/%s/metadata", path, 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 {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return err
+       }
+
+       if !r.HasExtension("container_edit_metadata") {
+               return fmt.Errorf("The server is missing the required 
\"container_edit_metadata\" API extension")
+       }
+
+       url := fmt.Sprintf("%s/%s/metadata", path, 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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       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("%s/%s/metadata/templates", path, 
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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       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%s/%s/metadata/templates?path=%s", 
r.httpHost, path, 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 {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return err
+       }
+
+       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%s/%s/metadata/templates?path=%s", 
r.httpHost, path, 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 {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return err
+       }
+
+       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("%s/%s/metadata/templates?path=%s", path, 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 *ContainerConsoleArgs) (Operation, error) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       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("%s/%s/console", 
path, 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 
*ContainerConsoleLogArgs) (io.ReadCloser, error) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       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%s/%s/console", r.httpHost, path, 
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 
*ContainerConsoleLogArgs) error {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return err
+       }
+
+       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("%s/%s/console", path, 
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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       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("%s/%s/backups", path, 
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("%s/%s/backups/", 
path, 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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       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("%s/%s/backups?recursion=1", 
path, 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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, "", err
+       }
+
+       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("%s/%s/backups/%s", path, 
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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       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("%s/%s/backups", 
path, 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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       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("%s/%s/backups/%s", 
path, 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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       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("%s/%s/backups/%s", path, 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) {
+       path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+       if err != nil {
+               return nil, err
+       }
+
+       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%s/%s/backups/%s/export", r.httpHost, path, 
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
+}

From 6b9769a8c026549a45f32228efe5fa69c3e6f262 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parr...@canonical.com>
Date: Thu, 12 Sep 2019 11:59:51 +0100
Subject: [PATCH 3/3] lxc: Switches cli tool to use InstanceServer

Signed-off-by: Thomas Parrott <thomas.parr...@canonical.com>
---
 lxc/action.go        |  4 ++--
 lxc/config/remote.go |  4 ++--
 lxc/console.go       |  2 +-
 lxc/copy.go          |  6 +++---
 lxc/delete.go        |  2 +-
 lxc/exec.go          |  2 +-
 lxc/export.go        |  2 +-
 lxc/file.go          |  8 ++++----
 lxc/image.go         |  2 +-
 lxc/info.go          |  6 +++---
 lxc/init.go          |  8 ++++----
 lxc/list.go          |  4 ++--
 lxc/main.go          |  6 +++---
 lxc/monitor.go       |  2 +-
 lxc/move.go          |  4 ++--
 lxc/project.go       |  2 +-
 lxc/publish.go       |  4 ++--
 lxc/query.go         |  2 +-
 lxc/remote.go        | 20 ++++++++++----------
 lxc/restore.go       |  2 +-
 lxc/snapshot.go      |  2 +-
 lxc/utils.go         |  6 +++---
 22 files changed, 50 insertions(+), 50 deletions(-)

diff --git a/lxc/action.go b/lxc/action.go
index e41e5f530e..a1ac453bad 100644
--- a/lxc/action.go
+++ b/lxc/action.go
@@ -142,7 +142,7 @@ func (c *cmdAction) doAction(action string, conf 
*config.Config, nameArg string)
                return err
        }
 
-       d, err := conf.GetContainerServer(remote)
+       d, err := conf.GetInstanceServer(remote)
        if err != nil {
                return err
        }
@@ -211,7 +211,7 @@ func (c *cmdAction) Run(cmd *cobra.Command, args []string) 
error {
                        return nil
                }
 
-               d, err := conf.GetContainerServer(conf.DefaultRemote)
+               d, err := conf.GetInstanceServer(conf.DefaultRemote)
                if err != nil {
                        return err
                }
diff --git a/lxc/config/remote.go b/lxc/config/remote.go
index cd7a7d8eca..40f64a9d9c 100644
--- a/lxc/config/remote.go
+++ b/lxc/config/remote.go
@@ -50,8 +50,8 @@ func (c *Config) ParseRemote(raw string) (string, string, 
error) {
        return result[0], result[1], nil
 }
 
-// GetContainerServer returns a ContainerServer struct for the remote
-func (c *Config) GetContainerServer(name string) (lxd.ContainerServer, error) {
+// GetInstanceServer returns a InstanceServer struct for the remote
+func (c *Config) GetInstanceServer(name string) (lxd.InstanceServer, error) {
        // Handle "local" on non-Linux
        if name == "local" && runtime.GOOS != "linux" {
                return nil, ErrNotLinux
diff --git a/lxc/console.go b/lxc/console.go
index 9404a0f89f..2b096e5074 100644
--- a/lxc/console.go
+++ b/lxc/console.go
@@ -120,7 +120,7 @@ func (c *cmdConsole) Run(cmd *cobra.Command, args []string) 
error {
                return err
        }
 
-       d, err := conf.GetContainerServer(remote)
+       d, err := conf.GetInstanceServer(remote)
        if err != nil {
                return err
        }
diff --git a/lxc/copy.go b/lxc/copy.go
index ae3c2ddba9..bd9d515888 100644
--- a/lxc/copy.go
+++ b/lxc/copy.go
@@ -88,19 +88,19 @@ func (c *cmdCopy) copyContainer(conf *config.Config, 
sourceResource string,
        }
 
        // Connect to the source host
-       source, err := conf.GetContainerServer(sourceRemote)
+       source, err := conf.GetInstanceServer(sourceRemote)
        if err != nil {
                return err
        }
 
        // Connect to the destination host
-       var dest lxd.ContainerServer
+       var dest lxd.InstanceServer
        if sourceRemote == destRemote {
                // Source and destination are the same
                dest = source
        } else {
                // Destination is different, connect to it
-               dest, err = conf.GetContainerServer(destRemote)
+               dest, err = conf.GetInstanceServer(destRemote)
                if err != nil {
                        return err
                }
diff --git a/lxc/delete.go b/lxc/delete.go
index 8b89fc05d0..c6103c6b15 100644
--- a/lxc/delete.go
+++ b/lxc/delete.go
@@ -50,7 +50,7 @@ func (c *cmdDelete) promptDelete(name string) error {
        return nil
 }
 
-func (c *cmdDelete) doDelete(d lxd.ContainerServer, name string) error {
+func (c *cmdDelete) doDelete(d lxd.InstanceServer, name string) error {
        var op lxd.Operation
        var err error
 
diff --git a/lxc/exec.go b/lxc/exec.go
index 9447848757..31d4df9583 100644
--- a/lxc/exec.go
+++ b/lxc/exec.go
@@ -115,7 +115,7 @@ func (c *cmdExec) Run(cmd *cobra.Command, args []string) 
error {
                return err
        }
 
-       d, err := conf.GetContainerServer(remote)
+       d, err := conf.GetInstanceServer(remote)
        if err != nil {
                return err
        }
diff --git a/lxc/export.go b/lxc/export.go
index 6266bbe0b5..29929f3e91 100644
--- a/lxc/export.go
+++ b/lxc/export.go
@@ -58,7 +58,7 @@ func (c *cmdExport) Run(cmd *cobra.Command, args []string) 
error {
                return err
        }
 
-       d, err := conf.GetContainerServer(remote)
+       d, err := conf.GetInstanceServer(remote)
        if err != nil {
                return err
        }
diff --git a/lxc/file.go b/lxc/file.go
index 0c03d3c1a6..1945db217b 100644
--- a/lxc/file.go
+++ b/lxc/file.go
@@ -36,7 +36,7 @@ type cmdFile struct {
        flagRecursive bool
 }
 
-func fileGetWrapper(server lxd.ContainerServer, container string, path string) 
(buf io.ReadCloser, resp *lxd.ContainerFileResponse, err error) {
+func fileGetWrapper(server lxd.InstanceServer, container string, path string) 
(buf io.ReadCloser, resp *lxd.ContainerFileResponse, err error) {
        // Signal handling
        chSignal := make(chan os.Signal)
        signal.Notify(chSignal, os.Interrupt)
@@ -648,7 +648,7 @@ func (c *cmdFilePush) Run(cmd *cobra.Command, args 
[]string) error {
        return nil
 }
 
-func (c *cmdFile) recursivePullFile(d lxd.ContainerServer, container string, p 
string, targetDir string) error {
+func (c *cmdFile) recursivePullFile(d lxd.InstanceServer, container string, p 
string, targetDir string) error {
        buf, resp, err := d.GetContainerFile(container, p)
        if err != nil {
                return err
@@ -723,7 +723,7 @@ func (c *cmdFile) recursivePullFile(d lxd.ContainerServer, 
container string, p s
        return nil
 }
 
-func (c *cmdFile) recursivePushFile(d lxd.ContainerServer, container string, 
source string, target string) error {
+func (c *cmdFile) recursivePushFile(d lxd.InstanceServer, container string, 
source string, target string) error {
        source = filepath.Clean(source)
        sourceDir, _ := filepath.Split(source)
        sourceLen := len(sourceDir)
@@ -821,7 +821,7 @@ func (c *cmdFile) recursivePushFile(d lxd.ContainerServer, 
container string, sou
        return filepath.Walk(source, sendFile)
 }
 
-func (c *cmdFile) recursiveMkdir(d lxd.ContainerServer, container string, p 
string, mode *os.FileMode, uid int64, gid int64) error {
+func (c *cmdFile) recursiveMkdir(d lxd.InstanceServer, container string, p 
string, mode *os.FileMode, uid int64, gid int64) error {
        /* special case, every container has a /, we don't need to do anything 
*/
        if p == "/" {
                return nil
diff --git a/lxc/image.go b/lxc/image.go
index 538a8775f6..61c589486b 100644
--- a/lxc/image.go
+++ b/lxc/image.go
@@ -656,7 +656,7 @@ func (c *cmdImageImport) Run(cmd *cobra.Command, args 
[]string) error {
                rootfsFile = shared.HostPath(filepath.Clean(rootfsFile))
        }
 
-       d, err := conf.GetContainerServer(remote)
+       d, err := conf.GetInstanceServer(remote)
        if err != nil {
                return err
        }
diff --git a/lxc/info.go b/lxc/info.go
index dce77937cf..f33f5fe2bd 100644
--- a/lxc/info.go
+++ b/lxc/info.go
@@ -69,7 +69,7 @@ func (c *cmdInfo) Run(cmd *cobra.Command, args []string) 
error {
                }
        }
 
-       d, err := conf.GetContainerServer(remote)
+       d, err := conf.GetInstanceServer(remote)
        if err != nil {
                return err
        }
@@ -302,7 +302,7 @@ func (c *cmdInfo) renderCPU(cpu api.ResourcesCPUSocket, 
prefix string) {
        }
 }
 
-func (c *cmdInfo) remoteInfo(d lxd.ContainerServer) error {
+func (c *cmdInfo) remoteInfo(d lxd.InstanceServer) error {
        // Targeting
        if c.flagTarget != "" {
                if !d.IsClustered() {
@@ -417,7 +417,7 @@ func (c *cmdInfo) remoteInfo(d lxd.ContainerServer) error {
        return nil
 }
 
-func (c *cmdInfo) containerInfo(d lxd.ContainerServer, remote config.Remote, 
name string, showLog bool) error {
+func (c *cmdInfo) containerInfo(d lxd.InstanceServer, remote config.Remote, 
name string, showLog bool) error {
        // Sanity checks
        if c.flagTarget != "" {
                return fmt.Errorf(i18n.G("--target cannot be used with 
containers"))
diff --git a/lxc/init.go b/lxc/init.go
index 4586bf0884..65b2b0276d 100644
--- a/lxc/init.go
+++ b/lxc/init.go
@@ -73,7 +73,7 @@ func (c *cmdInit) Run(cmd *cobra.Command, args []string) 
error {
        return err
 }
 
-func (c *cmdInit) create(conf *config.Config, args []string) 
(lxd.ContainerServer, string, error) {
+func (c *cmdInit) create(conf *config.Config, args []string) 
(lxd.InstanceServer, string, error) {
        var name string
        var image string
        var remote string
@@ -134,7 +134,7 @@ func (c *cmdInit) create(conf *config.Config, args 
[]string) (lxd.ContainerServe
                }
        }
 
-       d, err := conf.GetContainerServer(remote)
+       d, err := conf.GetInstanceServer(remote)
        if err != nil {
                return nil, "", err
        }
@@ -325,7 +325,7 @@ func (c *cmdInit) create(conf *config.Config, args 
[]string) (lxd.ContainerServe
        return d, name, nil
 }
 
-func (c *cmdInit) guessImage(conf *config.Config, d lxd.ContainerServer, 
remote string, iremote string, image string) (string, string) {
+func (c *cmdInit) guessImage(conf *config.Config, d lxd.InstanceServer, remote 
string, iremote string, image string) (string, string) {
        if remote != iremote {
                return iremote, image
        }
@@ -355,7 +355,7 @@ func (c *cmdInit) guessImage(conf *config.Config, d 
lxd.ContainerServer, remote
        return fields[0], fields[1]
 }
 
-func (c *cmdInit) checkNetwork(d lxd.ContainerServer, name string) {
+func (c *cmdInit) checkNetwork(d lxd.InstanceServer, name string) {
        ct, _, err := d.GetContainer(name)
        if err != nil {
                return
diff --git a/lxc/list.go b/lxc/list.go
index d3a484f83d..aaef88c392 100644
--- a/lxc/list.go
+++ b/lxc/list.go
@@ -201,7 +201,7 @@ func (c *cmdList) shouldShow(filters []string, state 
*api.Container) bool {
        return true
 }
 
-func (c *cmdList) listContainers(conf *config.Config, d lxd.ContainerServer, 
cinfos []api.Container, filters []string, columns []column) error {
+func (c *cmdList) listContainers(conf *config.Config, d lxd.InstanceServer, 
cinfos []api.Container, filters []string, columns []column) error {
        threads := 10
        if len(cinfos) < threads {
                threads = len(cinfos)
@@ -371,7 +371,7 @@ func (c *cmdList) Run(cmd *cobra.Command, args []string) 
error {
        }
 
        // Connect to LXD
-       d, err := conf.GetContainerServer(remote)
+       d, err := conf.GetInstanceServer(remote)
        if err != nil {
                return err
        }
diff --git a/lxc/main.go b/lxc/main.go
index afd68a23eb..c63d040273 100644
--- a/lxc/main.go
+++ b/lxc/main.go
@@ -365,12 +365,12 @@ func (c *cmdGlobal) PostRun(cmd *cobra.Command, args 
[]string) error {
 }
 
 type remoteResource struct {
-       server lxd.ContainerServer
+       server lxd.InstanceServer
        name   string
 }
 
 func (c *cmdGlobal) ParseServers(remotes ...string) ([]remoteResource, error) {
-       servers := map[string]lxd.ContainerServer{}
+       servers := map[string]lxd.InstanceServer{}
        resources := []remoteResource{}
 
        for _, remote := range remotes {
@@ -394,7 +394,7 @@ func (c *cmdGlobal) ParseServers(remotes ...string) 
([]remoteResource, error) {
                }
 
                // New connection
-               d, err := c.conf.GetContainerServer(remoteName)
+               d, err := c.conf.GetInstanceServer(remoteName)
                if err != nil {
                        return nil, err
                }
diff --git a/lxc/monitor.go b/lxc/monitor.go
index 8c633375f9..22f98cfc7b 100644
--- a/lxc/monitor.go
+++ b/lxc/monitor.go
@@ -74,7 +74,7 @@ func (c *cmdMonitor) Run(cmd *cobra.Command, args []string) 
error {
                }
        }
 
-       d, err := conf.GetContainerServer(remote)
+       d, err := conf.GetInstanceServer(remote)
        if err != nil {
                return err
        }
diff --git a/lxc/move.go b/lxc/move.go
index 5985b38932..5d12ec1204 100644
--- a/lxc/move.go
+++ b/lxc/move.go
@@ -108,7 +108,7 @@ func (c *cmdMove) Run(cmd *cobra.Command, args []string) 
error {
                        return fmt.Errorf(i18n.G("Can't override configuration 
or profiles in local rename"))
                }
 
-               source, err := conf.GetContainerServer(sourceRemote)
+               source, err := conf.GetInstanceServer(sourceRemote)
                if err != nil {
                        return err
                }
@@ -212,7 +212,7 @@ func moveClusterContainer(conf *config.Config, 
sourceResource, destResource, tar
        }
 
        // Connect to the source host
-       source, err := conf.GetContainerServer(sourceRemote)
+       source, err := conf.GetInstanceServer(sourceRemote)
        if err != nil {
                return errors.Wrap(err, i18n.G("Failed to connect to cluster 
member"))
        }
diff --git a/lxc/project.go b/lxc/project.go
index 0624c6fabe..35d2b5af7d 100644
--- a/lxc/project.go
+++ b/lxc/project.go
@@ -693,7 +693,7 @@ func (c *cmdProjectSwitch) Run(cmd *cobra.Command, args 
[]string) error {
        }
 
        // Make sure the project exists
-       d, err := conf.GetContainerServer(remote)
+       d, err := conf.GetInstanceServer(remote)
        if err != nil {
                return err
        }
diff --git a/lxc/publish.go b/lxc/publish.go
index bba6c5ceb4..3b5031779d 100644
--- a/lxc/publish.go
+++ b/lxc/publish.go
@@ -83,14 +83,14 @@ func (c *cmdPublish) Run(cmd *cobra.Command, args []string) 
error {
                return fmt.Errorf(i18n.G("There is no \"image name\".  Did you 
want an alias?"))
        }
 
-       d, err := conf.GetContainerServer(iRemote)
+       d, err := conf.GetInstanceServer(iRemote)
        if err != nil {
                return err
        }
 
        s := d
        if cRemote != iRemote {
-               s, err = conf.GetContainerServer(cRemote)
+               s, err = conf.GetInstanceServer(cRemote)
                if err != nil {
                        return err
                }
diff --git a/lxc/query.go b/lxc/query.go
index 5e8f948a5b..88180c98ed 100644
--- a/lxc/query.go
+++ b/lxc/query.go
@@ -68,7 +68,7 @@ func (c *cmdQuery) Run(cmd *cobra.Command, args []string) 
error {
        }
 
        // Attempt to connect
-       d, err := conf.GetContainerServer(remote)
+       d, err := conf.GetInstanceServer(remote)
        if err != nil {
                return err
        }
diff --git a/lxc/remote.go b/lxc/remote.go
index 62dbf0ff63..e6fd440da0 100644
--- a/lxc/remote.go
+++ b/lxc/remote.go
@@ -225,7 +225,7 @@ func (c *cmdRemoteAdd) Run(cmd *cobra.Command, args 
[]string) error {
        if c.flagPublic {
                d, err = conf.GetImageServer(server)
        } else {
-               d, err = conf.GetContainerServer(server)
+               d, err = conf.GetInstanceServer(server)
        }
 
        // Handle Unix socket connections
@@ -286,7 +286,7 @@ func (c *cmdRemoteAdd) Run(cmd *cobra.Command, args 
[]string) error {
                if c.flagPublic {
                        d, err = conf.GetImageServer(server)
                } else {
-                       d, err = conf.GetContainerServer(server)
+                       d, err = conf.GetInstanceServer(server)
                }
 
                if err != nil {
@@ -301,11 +301,11 @@ func (c *cmdRemoteAdd) Run(cmd *cobra.Command, args 
[]string) error {
        }
 
        if c.flagAuthType == "candid" {
-               d.(lxd.ContainerServer).RequireAuthenticated(false)
+               d.(lxd.InstanceServer).RequireAuthenticated(false)
        }
 
        // Get server information
-       srv, _, err := d.(lxd.ContainerServer).GetServer()
+       srv, _, err := d.(lxd.InstanceServer).GetServer()
        if err != nil {
                return err
        }
@@ -321,14 +321,14 @@ func (c *cmdRemoteAdd) Run(cmd *cobra.Command, args 
[]string) error {
                        conf.Remotes[server] = remote
 
                        // Re-setup the client
-                       d, err = conf.GetContainerServer(server)
+                       d, err = conf.GetInstanceServer(server)
                        if err != nil {
                                return err
                        }
 
-                       d.(lxd.ContainerServer).RequireAuthenticated(false)
+                       d.(lxd.InstanceServer).RequireAuthenticated(false)
 
-                       srv, _, err = d.(lxd.ContainerServer).GetServer()
+                       srv, _, err = d.(lxd.InstanceServer).GetServer()
                        if err != nil {
                                return err
                        }
@@ -380,16 +380,16 @@ func (c *cmdRemoteAdd) Run(cmd *cobra.Command, args 
[]string) error {
                }
                req.Type = "client"
 
-               err = d.(lxd.ContainerServer).CreateCertificate(req)
+               err = d.(lxd.InstanceServer).CreateCertificate(req)
                if err != nil {
                        return err
                }
        } else {
-               d.(lxd.ContainerServer).RequireAuthenticated(true)
+               d.(lxd.InstanceServer).RequireAuthenticated(true)
        }
 
        // And check if trusted now
-       srv, _, err = d.(lxd.ContainerServer).GetServer()
+       srv, _, err = d.(lxd.InstanceServer).GetServer()
        if err != nil {
                return err
        }
diff --git a/lxc/restore.go b/lxc/restore.go
index 2bbc26065e..acf134cb3d 100644
--- a/lxc/restore.go
+++ b/lxc/restore.go
@@ -53,7 +53,7 @@ func (c *cmdRestore) Run(cmd *cobra.Command, args []string) 
error {
                return err
        }
 
-       d, err := conf.GetContainerServer(remote)
+       d, err := conf.GetInstanceServer(remote)
        if err != nil {
                return err
        }
diff --git a/lxc/snapshot.go b/lxc/snapshot.go
index c98db99634..d5b008e16b 100644
--- a/lxc/snapshot.go
+++ b/lxc/snapshot.go
@@ -58,7 +58,7 @@ func (c *cmdSnapshot) Run(cmd *cobra.Command, args []string) 
error {
                return err
        }
 
-       d, err := conf.GetContainerServer(remote)
+       d, err := conf.GetInstanceServer(remote)
        if err != nil {
                return err
        }
diff --git a/lxc/utils.go b/lxc/utils.go
index 4de856bc1b..3c866b5c60 100644
--- a/lxc/utils.go
+++ b/lxc/utils.go
@@ -130,7 +130,7 @@ func runBatch(names []string, action func(name string) 
error) []batchResult {
 }
 
 // Add a device to a container
-func containerDeviceAdd(client lxd.ContainerServer, name string, devName 
string, dev map[string]string) error {
+func containerDeviceAdd(client lxd.InstanceServer, name string, devName 
string, dev map[string]string) error {
        // Get the container entry
        container, etag, err := client.GetContainer(name)
        if err != nil {
@@ -154,7 +154,7 @@ func containerDeviceAdd(client lxd.ContainerServer, name 
string, devName string,
 }
 
 // Add a device to a profile
-func profileDeviceAdd(client lxd.ContainerServer, name string, devName string, 
dev map[string]string) error {
+func profileDeviceAdd(client lxd.InstanceServer, name string, devName string, 
dev map[string]string) error {
        // Get the profile entry
        profile, profileEtag, err := client.GetProfile(name)
        if err != nil {
@@ -179,7 +179,7 @@ func profileDeviceAdd(client lxd.ContainerServer, name 
string, devName string, d
 }
 
 // Create the specified image alises, updating those that already exist
-func ensureImageAliases(client lxd.ContainerServer, aliases []api.ImageAlias, 
fingerprint string) error {
+func ensureImageAliases(client lxd.InstanceServer, aliases []api.ImageAlias, 
fingerprint string) error {
        if len(aliases) == 0 {
                return nil
        }
_______________________________________________
lxc-devel mailing list
lxc-devel@lists.linuxcontainers.org
http://lists.linuxcontainers.org/listinfo/lxc-devel

Reply via email to