[lxc-devel] [lxd/master] Client Instances 2 - rest
tomponline on Github
lxc-bot at linuxcontainers.org
Thu Sep 12 14:55:02 UTC 2019
A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 557 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20190912/285ce312/attachment-0001.bin>
-------------- next part --------------
From d7d30a08104061b1f74a51c92dcfcf184cd0c899 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Thu, 12 Sep 2019 11:57:02 +0100
Subject: [PATCH 1/6] client/connection: Adds connect functions that return
InstanceServer type
Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
client/connection.go | 30 +++++++++++++++++++++++++-----
1 file changed, 25 insertions(+), 5 deletions(-)
diff --git a/client/connection.go b/client/connection.go
index c9a25e1066..6cfa0fb199 100644
--- a/client/connection.go
+++ b/client/connection.go
@@ -51,14 +51,14 @@ type ConnectionArgs struct {
SkipGetServer bool
}
-// ConnectLXD lets you connect to a remote LXD daemon over HTTPs.
+// ConnectLXDInstance lets you connect to a remote LXD daemon over HTTPs.
//
// A client certificate (TLSClientCert) and key (TLSClientKey) must be provided.
//
// If connecting to a LXD daemon running in PKI mode, the PKI CA (TLSCA) must also be provided.
//
// Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert).
-func ConnectLXD(url string, args *ConnectionArgs) (ContainerServer, error) {
+func ConnectLXDInstance(url string, args *ConnectionArgs) (InstanceServer, error) {
logger.Debugf("Connecting to a remote LXD over HTTPs")
// Cleanup URL
@@ -67,12 +67,23 @@ func ConnectLXD(url string, args *ConnectionArgs) (ContainerServer, error) {
return httpsLXD(url, args)
}
-// ConnectLXDUnix lets you connect to a remote LXD daemon over a local unix socket.
+// ConnectLXD lets you connect to a remote LXD daemon over HTTPs.
+//
+// A client certificate (TLSClientCert) and key (TLSClientKey) must be provided.
+//
+// If connecting to a LXD daemon running in PKI mode, the PKI CA (TLSCA) must also be provided.
+//
+// Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert).
+func ConnectLXD(url string, args *ConnectionArgs) (ContainerServer, error) {
+ return ConnectLXDInstance(url, args)
+}
+
+// ConnectLXDInstanceUnix lets you connect to a remote LXD daemon over a local unix socket.
//
// If the path argument is empty, then $LXD_SOCKET will be used, if
// unset $LXD_DIR/unix.socket will be used and if that one isn't set
// either, then the path will default to /var/lib/lxd/unix.socket.
-func ConnectLXDUnix(path string, args *ConnectionArgs) (ContainerServer, error) {
+func ConnectLXDInstanceUnix(path string, args *ConnectionArgs) (InstanceServer, error) {
logger.Debugf("Connecting to a local LXD over a Unix socket")
// Use empty args if not specified
@@ -122,6 +133,15 @@ func ConnectLXDUnix(path string, args *ConnectionArgs) (ContainerServer, error)
return &server, nil
}
+// ConnectLXDUnix lets you connect to a remote LXD daemon over a local unix socket.
+//
+// If the path argument is empty, then $LXD_SOCKET will be used, if
+// unset $LXD_DIR/unix.socket will be used and if that one isn't set
+// either, then the path will default to /var/lib/lxd/unix.socket.
+func ConnectLXDUnix(path string, args *ConnectionArgs) (ContainerServer, error) {
+ return ConnectLXDInstanceUnix(path, args)
+}
+
// ConnectPublicLXD lets you connect to a remote public LXD daemon over HTTPs.
//
// Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert).
@@ -170,7 +190,7 @@ func ConnectSimpleStreams(url string, args *ConnectionArgs) (ImageServer, error)
}
// Internal function called by ConnectLXD and ConnectPublicLXD
-func httpsLXD(url string, args *ConnectionArgs) (ContainerServer, error) {
+func httpsLXD(url string, args *ConnectionArgs) (InstanceServer, error) {
// Use empty args if not specified
if args == nil {
args = &ConnectionArgs{}
From cd9619a83512a4ee03047fa2241531945abe2889 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Thu, 12 Sep 2019 11:57:49 +0100
Subject: [PATCH 2/6] client/interfaces: Creates InstanceServer interface
- Creates new arg types for instances and aliases existing types.
Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
client/interfaces.go | 68 +++++++++++++++++++++++++++++++++-----------
1 file changed, 52 insertions(+), 16 deletions(-)
diff --git a/client/interfaces.go b/client/interfaces.go
index 6e8cc7df3e..ae5cf82c9b 100644
--- a/client/interfaces.go
+++ b/client/interfaces.go
@@ -233,6 +233,16 @@ type ContainerServer interface {
RawOperation(method string, path string, data interface{}, queryETag string) (op Operation, ETag string, err error)
}
+// The InstanceServer type represents a full featured LXD server with Instances support.
+//
+// API extension: instances
+type InstanceServer interface {
+ ContainerServer
+
+ UseInstanceTarget(name string) (client InstanceServer)
+ UseInstanceProject(name string) (client InstanceServer)
+}
+
// The ConnectionInfo struct represents general information for a connection
type ConnectionInfo struct {
Addresses []string
@@ -243,8 +253,8 @@ type ConnectionInfo struct {
Project string
}
-// The ContainerBackupArgs struct is used when creating a container from a backup
-type ContainerBackupArgs struct {
+// The InstanceBackupArgs struct is used when creating a container from a backup
+type InstanceBackupArgs struct {
// The backup file
BackupFile io.Reader
@@ -252,6 +262,9 @@ type ContainerBackupArgs struct {
PoolName string
}
+// The ContainerBackupArgs struct is used when creating a container from a backup
+type ContainerBackupArgs InstanceBackupArgs
+
// The BackupFileRequest struct is used for a backup download request
type BackupFileRequest struct {
// Writer for the backup file
@@ -356,8 +369,8 @@ type StoragePoolVolumeMoveArgs struct {
StoragePoolVolumeCopyArgs
}
-// The ContainerCopyArgs struct is used to pass additional options during container copy
-type ContainerCopyArgs struct {
+// The InstanceCopyArgs struct is used to pass additional options during container copy
+type InstanceCopyArgs struct {
// If set, the container will be renamed on copy
Name string
@@ -375,8 +388,11 @@ type ContainerCopyArgs struct {
Refresh bool
}
-// The ContainerSnapshotCopyArgs struct is used to pass additional options during container copy
-type ContainerSnapshotCopyArgs struct {
+// The ContainerCopyArgs struct is used to pass additional options during container copy
+type ContainerCopyArgs InstanceCopyArgs
+
+// The InstanceSnapshotCopyArgs struct is used to pass additional options during container copy
+type InstanceSnapshotCopyArgs struct {
// If set, the container will be renamed on copy
Name string
@@ -388,9 +404,12 @@ type ContainerSnapshotCopyArgs struct {
Live bool
}
-// The ContainerConsoleArgs struct is used to pass additional options during a
+// The ContainerSnapshotCopyArgs struct is used to pass additional options during container copy
+type ContainerSnapshotCopyArgs InstanceSnapshotCopyArgs
+
+// The InstanceConsoleArgs struct is used to pass additional options during a
// container console session
-type ContainerConsoleArgs struct {
+type InstanceConsoleArgs struct {
// Bidirectional fd to pass to the container
Terminal io.ReadWriteCloser
@@ -401,13 +420,21 @@ type ContainerConsoleArgs struct {
ConsoleDisconnect chan bool
}
-// The ContainerConsoleLogArgs struct is used to pass additional options during a
+// The ContainerConsoleArgs struct is used to pass additional options during a
+// container console session
+type ContainerConsoleArgs InstanceConsoleArgs
+
+// The InstanceConsoleLogArgs struct is used to pass additional options during a
// container console log request
-type ContainerConsoleLogArgs struct {
+type InstanceConsoleLogArgs struct {
}
-// The ContainerExecArgs struct is used to pass additional options during container exec
-type ContainerExecArgs struct {
+// The ContainerConsoleLogArgs struct is used to pass additional options during a
+// container console log request
+type ContainerConsoleLogArgs InstanceConsoleLogArgs
+
+// The InstanceExecArgs struct is used to pass additional options during container exec
+type InstanceExecArgs struct {
// Standard input
Stdin io.ReadCloser
@@ -424,8 +451,11 @@ type ContainerExecArgs struct {
DataDone chan bool
}
-// The ContainerFileArgs struct is used to pass the various options for a container file upload
-type ContainerFileArgs struct {
+// The ContainerExecArgs struct is used to pass additional options during container exec
+type ContainerExecArgs InstanceExecArgs
+
+// The InstanceFileArgs struct is used to pass the various options for a container file upload
+type InstanceFileArgs struct {
// File content
Content io.ReadSeeker
@@ -445,8 +475,11 @@ type ContainerFileArgs struct {
WriteMode string
}
-// The ContainerFileResponse struct is used as part of the response for a container file download
-type ContainerFileResponse struct {
+// The ContainerFileArgs struct is used to pass the various options for a container file upload
+type ContainerFileArgs InstanceFileArgs
+
+// The InstanceFileResponse struct is used as part of the response for a container file download
+type InstanceFileResponse struct {
// User id that owns the file
UID int64
@@ -462,3 +495,6 @@ type ContainerFileResponse struct {
// If a directory, the list of files inside it
Entries []string
}
+
+// The ContainerFileResponse struct is used as part of the response for a container file download
+type ContainerFileResponse InstanceFileResponse
From 7676ee551c5e54f0c05e1ccb95ecabd24109c6c7 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Thu, 12 Sep 2019 11:58:41 +0100
Subject: [PATCH 3/6] client/lxd/server: Adds UseInstanceTarget and
UseInstanceProject functions
And aliases existing functions to return old type.
Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
client/lxd_server.go | 20 ++++++++++++++++----
1 file changed, 16 insertions(+), 4 deletions(-)
diff --git a/client/lxd_server.go b/client/lxd_server.go
index 4cc20cc1c6..c1f3ec656c 100644
--- a/client/lxd_server.go
+++ b/client/lxd_server.go
@@ -89,8 +89,8 @@ func (r *ProtocolLXD) GetServerResources() (*api.Resources, error) {
return &resources, nil
}
-// UseProject returns a client that will use a specific project.
-func (r *ProtocolLXD) UseProject(name string) ContainerServer {
+// UseInstanceProject returns a client that will use a specific project.
+func (r *ProtocolLXD) UseInstanceProject(name string) InstanceServer {
return &ProtocolLXD{
server: r.server,
http: r.http,
@@ -106,10 +106,15 @@ func (r *ProtocolLXD) UseProject(name string) ContainerServer {
}
}
-// UseTarget returns a client that will target a specific cluster member.
+// UseProject returns a client that will use a specific project.
+func (r *ProtocolLXD) UseProject(name string) ContainerServer {
+ return ContainerServer(r.UseInstanceProject(name))
+}
+
+// UseInstanceTarget returns a client that will target a specific cluster member.
// Use this member-specific operations such as specific container
// placement, preparing a new storage pool or network, ...
-func (r *ProtocolLXD) UseTarget(name string) ContainerServer {
+func (r *ProtocolLXD) UseInstanceTarget(name string) InstanceServer {
return &ProtocolLXD{
server: r.server,
http: r.http,
@@ -124,3 +129,10 @@ func (r *ProtocolLXD) UseTarget(name string) ContainerServer {
clusterTarget: name,
}
}
+
+// UseTarget returns a client that will target a specific cluster member.
+// Use this member-specific operations such as specific container
+// placement, preparing a new storage pool or network, ...
+func (r *ProtocolLXD) UseTarget(name string) ContainerServer {
+ return ContainerServer(r.UseInstanceTarget(name))
+}
From 8bf45f7e90846a938f690501afdd2047ab2dbac1 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Thu, 12 Sep 2019 15:46:44 +0100
Subject: [PATCH 4/6] client/interfaces: Populates InstanceServer with rest of
functions
Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
client/interfaces.go | 59 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 59 insertions(+)
diff --git a/client/interfaces.go b/client/interfaces.go
index ae5cf82c9b..7ffce04c88 100644
--- a/client/interfaces.go
+++ b/client/interfaces.go
@@ -241,6 +241,65 @@ type InstanceServer interface {
UseInstanceTarget(name string) (client InstanceServer)
UseInstanceProject(name string) (client InstanceServer)
+
+ // Instance functions.
+ GetInstanceNames() (names []string, err error)
+ GetInstances() (instances []api.Instance, err error)
+ GetInstancesFull() (instances []api.InstanceFull, err error)
+ GetInstance(name string) (instance *api.Instance, ETag string, err error)
+ CreateInstance(instance api.InstancesPost) (op Operation, err error)
+ CreateInstanceFromImage(source ImageServer, image api.Image, imgcontainer api.InstancesPost) (op RemoteOperation, err error)
+ CopyInstance(source InstanceServer, instance api.Instance, args *InstanceCopyArgs) (op RemoteOperation, err error)
+ UpdateInstance(name string, instance api.InstancePut, ETag string) (op Operation, err error)
+ RenameInstance(name string, instance api.InstancePost) (op Operation, err error)
+ MigrateInstance(name string, instance api.InstancePost) (op Operation, err error)
+ DeleteInstance(name string) (op Operation, err error)
+
+ ExecInstance(instanceName string, exec api.InstanceExecPost, args *InstanceExecArgs) (op Operation, err error)
+ ConsoleInstance(instanceName string, console api.InstanceConsolePost, args *InstanceConsoleArgs) (op Operation, err error)
+ GetInstanceConsoleLog(instanceName string, args *InstanceConsoleLogArgs) (content io.ReadCloser, err error)
+ DeleteInstanceConsoleLog(instanceName string, args *InstanceConsoleLogArgs) (err error)
+
+ GetInstanceFile(instanceName string, path string) (content io.ReadCloser, resp *InstanceFileResponse, err error)
+ CreateInstanceFile(instanceName string, path string, args InstanceFileArgs) (err error)
+ DeleteInstanceFile(instanceName string, path string) (err error)
+
+ GetInstanceSnapshotNames(instanceName string) (names []string, err error)
+ GetInstanceSnapshots(instanceName string) (snapshots []api.InstanceSnapshot, err error)
+ GetInstanceSnapshot(instanceName string, name string) (snapshot *api.InstanceSnapshot, ETag string, err error)
+ CreateInstanceSnapshot(instanceName string, snapshot api.InstanceSnapshotsPost) (op Operation, err error)
+ CopyInstanceSnapshot(source InstanceServer, instanceName string, snapshot api.InstanceSnapshot, args *InstanceSnapshotCopyArgs) (op RemoteOperation, err error)
+ RenameInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPost) (op Operation, err error)
+ MigrateInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPost) (op Operation, err error)
+ DeleteInstanceSnapshot(instanceName string, name string) (op Operation, err error)
+ UpdateInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPut, ETag string) (op Operation, err error)
+
+ GetInstanceBackupNames(instanceName string) (names []string, err error)
+ GetInstanceBackups(instanceName string) (backups []api.InstanceBackup, err error)
+ GetInstanceBackup(instanceName string, name string) (backup *api.InstanceBackup, ETag string, err error)
+ CreateInstanceBackup(instanceName string, backup api.InstanceBackupsPost) (op Operation, err error)
+ RenameInstanceBackup(instanceName string, name string, backup api.InstanceBackupPost) (op Operation, err error)
+ DeleteInstanceBackup(instanceName string, name string) (op Operation, err error)
+ GetInstanceBackupFile(instanceName string, name string, req *BackupFileRequest) (resp *BackupFileResponse, err error)
+ CreateInstanceFromBackup(args InstanceBackupArgs) (op Operation, err error)
+
+ GetInstanceState(name string) (state *api.InstanceState, ETag string, err error)
+ UpdateInstanceState(name string, state api.InstanceStatePut, ETag string) (op Operation, err error)
+
+ GetInstanceLogfiles(name string) (logfiles []string, err error)
+ GetInstanceLogfile(name string, filename string) (content io.ReadCloser, err error)
+ DeleteInstanceLogfile(name string, filename string) (err error)
+
+ GetInstanceMetadata(name string) (metadata *api.ImageMetadata, ETag string, err error)
+ SetInstanceMetadata(name string, metadata api.ImageMetadata, ETag string) (err error)
+
+ GetInstanceTemplateFiles(instanceName string) (templates []string, err error)
+ GetInstanceTemplateFile(instanceName string, templateName string) (content io.ReadCloser, err error)
+ CreateInstanceTemplateFile(instanceName string, templateName string, content io.ReadSeeker) (err error)
+ UpdateInstanceTemplateFile(instanceName string, templateName string, content io.ReadSeeker) (err error)
+
+ CopyInstanceStoragePoolVolume(pool string, source InstanceServer, sourcePool string, volume api.StorageVolume, args *StoragePoolVolumeCopyArgs) (op RemoteOperation, err error)
+ MoveInstanceStoragePoolVolume(pool string, source InstanceServer, sourcePool string, volume api.StorageVolume, args *StoragePoolVolumeMoveArgs) (op RemoteOperation, err error)
}
// The ConnectionInfo struct represents general information for a connection
From c4adfff71afccca7c157d55633423130695751d0 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Thu, 12 Sep 2019 15:47:41 +0100
Subject: [PATCH 5/6] client/lxd/storage/volumes: Adds storage management shims
to implement InstanceServer
- CopyInstanceStoragePoolVolume
- MoveInstanceStoragePoolVolume
Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
client/lxd_storage_volumes.go | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/client/lxd_storage_volumes.go b/client/lxd_storage_volumes.go
index 291ad4f3e2..1f11296558 100644
--- a/client/lxd_storage_volumes.go
+++ b/client/lxd_storage_volumes.go
@@ -519,6 +519,11 @@ func (r *ProtocolLXD) CopyStoragePoolVolume(pool string, source ContainerServer,
return r.tryCreateStoragePoolVolume(pool, req, info.Addresses)
}
+// CopyInstanceStoragePoolVolume copies an existing instance server storage volume.
+func (r *ProtocolLXD) CopyInstanceStoragePoolVolume(pool string, source InstanceServer, sourcePool string, volume api.StorageVolume, args *StoragePoolVolumeCopyArgs) (RemoteOperation, error) {
+ return r.CopyStoragePoolVolume(pool, source, sourcePool, volume, args)
+}
+
// MoveStoragePoolVolume renames or moves an existing storage volume
func (r *ProtocolLXD) MoveStoragePoolVolume(pool string, source ContainerServer, sourcePool string, volume api.StorageVolume, args *StoragePoolVolumeMoveArgs) (RemoteOperation, error) {
if !r.HasExtension("storage_api_local_volume_handling") {
@@ -554,6 +559,11 @@ func (r *ProtocolLXD) MoveStoragePoolVolume(pool string, source ContainerServer,
return &rop, nil
}
+// MoveInstanceStoragePoolVolume renames or moves an existing instance server storage volume.
+func (r *ProtocolLXD) MoveInstanceStoragePoolVolume(pool string, source InstanceServer, sourcePool string, volume api.StorageVolume, args *StoragePoolVolumeMoveArgs) (RemoteOperation, error) {
+ return r.MoveStoragePoolVolume(pool, source, sourcePool, volume, args)
+}
+
// UpdateStoragePoolVolume updates the volume to match the provided StoragePoolVolume struct
func (r *ProtocolLXD) UpdateStoragePoolVolume(pool string, volType string, name string, volume api.StorageVolumePut, ETag string) error {
if !r.HasExtension("storage") {
From 8f2d6659ad7e9cc13f5eb568ff55174b2621e380 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Thu, 12 Sep 2019 15:50:34 +0100
Subject: [PATCH 6/6] client/lxd/instances: Adds instance related functions
These functions use the /1.0/instances endpoints on the server.
Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
client/lxd_instances.go | 1915 +++++++++++++++++++++++++++++++++++++++
1 file changed, 1915 insertions(+)
create mode 100644 client/lxd_instances.go
diff --git a/client/lxd_instances.go b/client/lxd_instances.go
new file mode 100644
index 0000000000..911754542f
--- /dev/null
+++ b/client/lxd_instances.go
@@ -0,0 +1,1915 @@
+package lxd
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+
+ "github.com/gorilla/websocket"
+
+ "github.com/lxc/lxd/shared"
+ "github.com/lxc/lxd/shared/api"
+ "github.com/lxc/lxd/shared/cancel"
+ "github.com/lxc/lxd/shared/ioprogress"
+ "github.com/lxc/lxd/shared/units"
+)
+
+// Instance handling functions.
+
+// GetInstanceNames returns a list of instance names.
+func (r *ProtocolLXD) GetInstanceNames() ([]string, error) {
+ urls := []string{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", "/instances", nil, "", &urls)
+ if err != nil {
+ return nil, err
+ }
+
+ // Parse it
+ names := []string{}
+ for _, url := range urls {
+ fields := strings.Split(url, "/instances/")
+ names = append(names, fields[len(fields)-1])
+ }
+
+ return names, nil
+}
+
+// GetInstances returns a list of instances.
+func (r *ProtocolLXD) GetInstances() ([]api.Instance, error) {
+ instances := []api.Instance{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", "/instances?recursion=1", nil, "", &instances)
+ if err != nil {
+ return nil, err
+ }
+
+ return instances, nil
+}
+
+// GetInstancesFull returns a list of instances including snapshots, backups and state.
+func (r *ProtocolLXD) GetInstancesFull() ([]api.InstanceFull, error) {
+ instances := []api.InstanceFull{}
+
+ if !r.HasExtension("container_full") {
+ return nil, fmt.Errorf("The server is missing the required \"container_full\" API extension")
+ }
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", "/instances?recursion=2", nil, "", &instances)
+ if err != nil {
+ return nil, err
+ }
+
+ return instances, nil
+}
+
+// GetInstance returns the instance entry for the provided name.
+func (r *ProtocolLXD) GetInstance(name string) (*api.Instance, string, error) {
+ instance := api.Instance{}
+
+ // Fetch the raw value
+ etag, err := r.queryStruct("GET", fmt.Sprintf("/instances/%s", url.PathEscape(name)), nil, "", &instance)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return &instance, etag, nil
+}
+
+// CreateInstanceFromBackup is a convenience function to make it easier to
+// create a instance from a backup
+func (r *ProtocolLXD) CreateInstanceFromBackup(args InstanceBackupArgs) (Operation, error) {
+ if !r.HasExtension("container_backup") {
+ return nil, fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+ }
+
+ if args.PoolName == "" {
+ // Send the request
+ op, _, err := r.queryOperation("POST", "/instances", args.BackupFile, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+ }
+
+ if !r.HasExtension("container_backup_override_pool") {
+ return nil, fmt.Errorf("The server is missing the required \"container_backup_override_pool\" API extension")
+ }
+
+ // Prepare the HTTP request
+ reqURL, err := r.setQueryAttributes(fmt.Sprintf("%s/1.0/instances", r.httpHost))
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("POST", reqURL, args.BackupFile)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Set("Content-Type", "application/octet-stream")
+ req.Header.Set("X-LXD-pool", args.PoolName)
+
+ // Set the user agent
+ if r.httpUserAgent != "" {
+ req.Header.Set("User-Agent", r.httpUserAgent)
+ }
+
+ // Send the request
+ resp, err := r.do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ // Handle errors
+ response, _, err := lxdParseResponse(resp)
+ if err != nil {
+ return nil, err
+ }
+
+ // Get to the operation
+ respOperation, err := response.MetadataAsOperation()
+ if err != nil {
+ return nil, err
+ }
+
+ // Setup an Operation wrapper
+ op := operation{
+ Operation: *respOperation,
+ r: r,
+ chActive: make(chan bool),
+ }
+
+ return &op, nil
+}
+
+// CreateInstance requests that LXD creates a new instance.
+func (r *ProtocolLXD) CreateInstance(instance api.InstancesPost) (Operation, error) {
+ if instance.Source.ContainerOnly {
+ if !r.HasExtension("container_only_migration") {
+ return nil, fmt.Errorf("The server is missing the required \"container_only_migration\" API extension")
+ }
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", "/instances", instance, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+func (r *ProtocolLXD) tryCreateInstance(req api.InstancesPost, urls []string) (RemoteOperation, error) {
+ if len(urls) == 0 {
+ return nil, fmt.Errorf("The source server isn't listening on the network")
+ }
+
+ rop := remoteOperation{
+ chDone: make(chan bool),
+ }
+
+ operation := req.Source.Operation
+
+ // Forward targetOp to remote op
+ go func() {
+ success := false
+ errors := map[string]error{}
+ for _, serverURL := range urls {
+ if operation == "" {
+ req.Source.Server = serverURL
+ } else {
+ req.Source.Operation = fmt.Sprintf("%s/1.0/operations/%s", serverURL, url.PathEscape(operation))
+ }
+
+ op, err := r.CreateInstance(req)
+ if err != nil {
+ errors[serverURL] = err
+ continue
+ }
+
+ rop.targetOp = op
+
+ for _, handler := range rop.handlers {
+ rop.targetOp.AddHandler(handler)
+ }
+
+ err = rop.targetOp.Wait()
+ if err != nil {
+ errors[serverURL] = err
+ continue
+ }
+
+ success = true
+ break
+ }
+
+ if !success {
+ rop.err = remoteOperationError("Failed instance creation", errors)
+ }
+
+ close(rop.chDone)
+ }()
+
+ return &rop, nil
+}
+
+// CreateInstanceFromImage is a convenience function to make it easier to create a instance from an existing image.
+func (r *ProtocolLXD) CreateInstanceFromImage(source ImageServer, image api.Image, req api.InstancesPost) (RemoteOperation, error) {
+ // Set the minimal source fields
+ req.Source.Type = "image"
+
+ // Optimization for the local image case
+ if r == source {
+ // Always use fingerprints for local case
+ req.Source.Fingerprint = image.Fingerprint
+ req.Source.Alias = ""
+
+ op, err := r.CreateInstance(req)
+ if err != nil {
+ return nil, err
+ }
+
+ rop := remoteOperation{
+ targetOp: op,
+ chDone: make(chan bool),
+ }
+
+ // Forward targetOp to remote op
+ go func() {
+ rop.err = rop.targetOp.Wait()
+ close(rop.chDone)
+ }()
+
+ return &rop, nil
+ }
+
+ // Minimal source fields for remote image
+ req.Source.Mode = "pull"
+
+ // If we have an alias and the image is public, use that
+ if req.Source.Alias != "" && image.Public {
+ req.Source.Fingerprint = ""
+ } else {
+ req.Source.Fingerprint = image.Fingerprint
+ req.Source.Alias = ""
+ }
+
+ // Get source server connection information
+ info, err := source.GetConnectionInfo()
+ if err != nil {
+ return nil, err
+ }
+
+ req.Source.Protocol = info.Protocol
+ req.Source.Certificate = info.Certificate
+
+ // Generate secret token if needed
+ if !image.Public {
+ secret, err := source.GetImageSecret(image.Fingerprint)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Source.Secret = secret
+ }
+
+ return r.tryCreateInstance(req, info.Addresses)
+}
+
+// CopyInstance copies a instance from a remote server. Additional options can be passed using InstanceCopyArgs.
+func (r *ProtocolLXD) CopyInstance(source InstanceServer, instance api.Instance, args *InstanceCopyArgs) (RemoteOperation, error) {
+ // Base request
+ req := api.InstancesPost{
+ Name: instance.Name,
+ InstancePut: instance.Writable(),
+ }
+ req.Source.BaseImage = instance.Config["volatile.base_image"]
+
+ // Process the copy arguments
+ if args != nil {
+ // Sanity checks
+ if args.ContainerOnly {
+ if !r.HasExtension("container_only_migration") {
+ return nil, fmt.Errorf("The target server is missing the required \"container_only_migration\" API extension")
+ }
+
+ if !source.HasExtension("container_only_migration") {
+ return nil, fmt.Errorf("The source server is missing the required \"container_only_migration\" API extension")
+ }
+ }
+
+ if shared.StringInSlice(args.Mode, []string{"push", "relay"}) {
+ if !r.HasExtension("container_push") {
+ return nil, fmt.Errorf("The target server is missing the required \"container_push\" API extension")
+ }
+
+ if !source.HasExtension("container_push") {
+ return nil, fmt.Errorf("The source server is missing the required \"container_push\" API extension")
+ }
+ }
+
+ if args.Mode == "push" && !source.HasExtension("container_push_target") {
+ return nil, fmt.Errorf("The source server is missing the required \"container_push_target\" API extension")
+ }
+
+ if args.Refresh {
+ if !r.HasExtension("container_incremental_copy") {
+ return nil, fmt.Errorf("The target server is missing the required \"container_incremental_copy\" API extension")
+ }
+
+ if !source.HasExtension("container_incremental_copy") {
+ return nil, fmt.Errorf("The source server is missing the required \"container_incremental_copy\" API extension")
+ }
+ }
+
+ // Allow overriding the target name
+ if args.Name != "" {
+ req.Name = args.Name
+ }
+
+ req.Source.Live = args.Live
+ req.Source.ContainerOnly = args.ContainerOnly
+ req.Source.Refresh = args.Refresh
+ }
+
+ if req.Source.Live {
+ req.Source.Live = instance.StatusCode == api.Running
+ }
+
+ sourceInfo, err := source.GetConnectionInfo()
+ if err != nil {
+ return nil, fmt.Errorf("Failed to get source connection info: %v", err)
+ }
+
+ destInfo, err := r.GetConnectionInfo()
+ if err != nil {
+ return nil, fmt.Errorf("Failed to get destination connection info: %v", err)
+ }
+
+ // Optimization for the local copy case
+ if destInfo.URL == sourceInfo.URL && destInfo.SocketPath == sourceInfo.SocketPath && (!r.IsClustered() || instance.Location == r.clusterTarget || r.HasExtension("cluster_internal_copy")) {
+ // Project handling
+ if destInfo.Project != sourceInfo.Project {
+ if !r.HasExtension("container_copy_project") {
+ return nil, fmt.Errorf("The server is missing the required \"container_copy_project\" API extension")
+ }
+
+ req.Source.Project = sourceInfo.Project
+ }
+
+ // Local copy source fields
+ req.Source.Type = "copy"
+ req.Source.Source = instance.Name
+
+ // Copy the instance
+ op, err := r.CreateInstance(req)
+ if err != nil {
+ return nil, err
+ }
+
+ rop := remoteOperation{
+ targetOp: op,
+ chDone: make(chan bool),
+ }
+
+ // Forward targetOp to remote op
+ go func() {
+ rop.err = rop.targetOp.Wait()
+ close(rop.chDone)
+ }()
+
+ return &rop, nil
+ }
+
+ // Source request
+ sourceReq := api.InstancePost{
+ Migration: true,
+ Live: req.Source.Live,
+ ContainerOnly: req.Source.ContainerOnly,
+ }
+
+ // Push mode migration
+ if args != nil && args.Mode == "push" {
+ // Get target server connection information
+ info, err := r.GetConnectionInfo()
+ if err != nil {
+ return nil, err
+ }
+
+ // Create the instance
+ req.Source.Type = "migration"
+ req.Source.Mode = "push"
+ req.Source.Refresh = args.Refresh
+
+ op, err := r.CreateInstance(req)
+ if err != nil {
+ return nil, err
+ }
+
+ opAPI := op.Get()
+
+ targetSecrets := map[string]string{}
+ for k, v := range opAPI.Metadata {
+ targetSecrets[k] = v.(string)
+ }
+
+ // Prepare the source request
+ target := api.InstancePostTarget{}
+ target.Operation = opAPI.ID
+ target.Websockets = targetSecrets
+ target.Certificate = info.Certificate
+ sourceReq.Target = &target
+
+ return r.tryMigrateInstance(source, instance.Name, sourceReq, info.Addresses)
+ }
+
+ // Get source server connection information
+ info, err := source.GetConnectionInfo()
+ if err != nil {
+ return nil, err
+ }
+
+ op, err := source.MigrateInstance(instance.Name, sourceReq)
+ if err != nil {
+ return nil, err
+ }
+ opAPI := op.Get()
+
+ sourceSecrets := map[string]string{}
+ for k, v := range opAPI.Metadata {
+ sourceSecrets[k] = v.(string)
+ }
+
+ // Relay mode migration
+ if args != nil && args.Mode == "relay" {
+ // Push copy source fields
+ req.Source.Type = "migration"
+ req.Source.Mode = "push"
+
+ // Start the process
+ targetOp, err := r.CreateInstance(req)
+ if err != nil {
+ return nil, err
+ }
+ targetOpAPI := targetOp.Get()
+
+ // Extract the websockets
+ targetSecrets := map[string]string{}
+ for k, v := range targetOpAPI.Metadata {
+ targetSecrets[k] = v.(string)
+ }
+
+ // Launch the relay
+ err = r.proxyMigration(targetOp.(*operation), targetSecrets, source, op.(*operation), sourceSecrets)
+ if err != nil {
+ return nil, err
+ }
+
+ // Prepare a tracking operation
+ rop := remoteOperation{
+ targetOp: targetOp,
+ chDone: make(chan bool),
+ }
+
+ // Forward targetOp to remote op
+ go func() {
+ rop.err = rop.targetOp.Wait()
+ close(rop.chDone)
+ }()
+
+ return &rop, nil
+ }
+
+ // Pull mode migration
+ req.Source.Type = "migration"
+ req.Source.Mode = "pull"
+ req.Source.Operation = opAPI.ID
+ req.Source.Websockets = sourceSecrets
+ req.Source.Certificate = info.Certificate
+
+ return r.tryCreateInstance(req, info.Addresses)
+}
+
+func (r *ProtocolLXD) proxyInstanceMigration(targetOp *operation, targetSecrets map[string]string, source InstanceServer, sourceOp *operation, sourceSecrets map[string]string) error {
+ // Sanity checks
+ for n := range targetSecrets {
+ _, ok := sourceSecrets[n]
+ if !ok {
+ return fmt.Errorf("Migration target expects the \"%s\" socket but source isn't providing it", n)
+ }
+ }
+
+ if targetSecrets["control"] == "" {
+ return fmt.Errorf("Migration target didn't setup the required \"control\" socket")
+ }
+
+ // Struct used to hold everything together
+ type proxy struct {
+ done chan bool
+ sourceConn *websocket.Conn
+ targetConn *websocket.Conn
+ }
+
+ proxies := map[string]*proxy{}
+
+ // Connect the control socket
+ sourceConn, err := source.GetOperationWebsocket(sourceOp.ID, sourceSecrets["control"])
+ if err != nil {
+ return err
+ }
+
+ targetConn, err := r.GetOperationWebsocket(targetOp.ID, targetSecrets["control"])
+ if err != nil {
+ return err
+ }
+
+ proxies["control"] = &proxy{
+ done: shared.WebsocketProxy(sourceConn, targetConn),
+ sourceConn: sourceConn,
+ targetConn: targetConn,
+ }
+
+ // Connect the data sockets
+ for name := range sourceSecrets {
+ if name == "control" {
+ continue
+ }
+
+ // Handle resets (used for multiple objects)
+ sourceConn, err := source.GetOperationWebsocket(sourceOp.ID, sourceSecrets[name])
+ if err != nil {
+ break
+ }
+
+ targetConn, err := r.GetOperationWebsocket(targetOp.ID, targetSecrets[name])
+ if err != nil {
+ break
+ }
+
+ proxies[name] = &proxy{
+ sourceConn: sourceConn,
+ targetConn: targetConn,
+ done: shared.WebsocketProxy(sourceConn, targetConn),
+ }
+ }
+
+ // Cleanup once everything is done
+ go func() {
+ // Wait for control socket
+ <-proxies["control"].done
+ proxies["control"].sourceConn.Close()
+ proxies["control"].targetConn.Close()
+
+ // Then deal with the others
+ for name, proxy := range proxies {
+ if name == "control" {
+ continue
+ }
+
+ <-proxy.done
+ proxy.sourceConn.Close()
+ proxy.targetConn.Close()
+ }
+ }()
+
+ return nil
+}
+
+// UpdateInstance updates the instance definition.
+func (r *ProtocolLXD) UpdateInstance(name string, instance api.InstancePut, ETag string) (Operation, error) {
+ // Send the request
+ op, _, err := r.queryOperation("PUT", fmt.Sprintf("/instances/%s", url.PathEscape(name)), instance, ETag)
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// RenameInstance requests that LXD renames the instance.
+func (r *ProtocolLXD) RenameInstance(name string, instance api.InstancePost) (Operation, error) {
+ // Sanity check
+ if instance.Migration {
+ return nil, fmt.Errorf("Can't ask for a migration through RenameInstance")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("/instances/%s", url.PathEscape(name)), instance, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+func (r *ProtocolLXD) tryMigrateInstance(source InstanceServer, name string, req api.InstancePost, urls []string) (RemoteOperation, error) {
+ if len(urls) == 0 {
+ return nil, fmt.Errorf("The target server isn't listening on the network")
+ }
+
+ rop := remoteOperation{
+ chDone: make(chan bool),
+ }
+
+ operation := req.Target.Operation
+
+ // Forward targetOp to remote op
+ go func() {
+ success := false
+ errors := map[string]error{}
+ for _, serverURL := range urls {
+ req.Target.Operation = fmt.Sprintf("%s/1.0/operations/%s", serverURL, url.PathEscape(operation))
+
+ op, err := source.MigrateInstance(name, req)
+ if err != nil {
+ errors[serverURL] = err
+ continue
+ }
+
+ rop.targetOp = op
+
+ for _, handler := range rop.handlers {
+ rop.targetOp.AddHandler(handler)
+ }
+
+ err = rop.targetOp.Wait()
+ if err != nil {
+ errors[serverURL] = err
+ continue
+ }
+
+ success = true
+ break
+ }
+
+ if !success {
+ rop.err = remoteOperationError("Failed instance migration", errors)
+ }
+
+ close(rop.chDone)
+ }()
+
+ return &rop, nil
+}
+
+// MigrateInstance requests that LXD prepares for a instance migration.
+func (r *ProtocolLXD) MigrateInstance(name string, instance api.InstancePost) (Operation, error) {
+ if instance.ContainerOnly {
+ if !r.HasExtension("container_only_migration") {
+ return nil, fmt.Errorf("The server is missing the required \"container_only_migration\" API extension")
+ }
+ }
+
+ // Sanity check
+ if !instance.Migration {
+ return nil, fmt.Errorf("Can't ask for a rename through MigrateInstance")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("/instances/%s", url.PathEscape(name)), instance, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// DeleteInstance requests that LXD deletes the instance.
+func (r *ProtocolLXD) DeleteInstance(name string) (Operation, error) {
+ // Send the request
+ op, _, err := r.queryOperation("DELETE", fmt.Sprintf("/instances/%s", url.PathEscape(name)), nil, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// ExecInstance requests that LXD spawns a command inside the instance.
+func (r *ProtocolLXD) ExecInstance(instanceName string, exec api.InstanceExecPost, args *InstanceExecArgs) (Operation, error) {
+ if exec.RecordOutput {
+ if !r.HasExtension("container_exec_recording") {
+ return nil, fmt.Errorf("The server is missing the required \"container_exec_recording\" API extension")
+ }
+ }
+
+ if exec.User > 0 || exec.Group > 0 || exec.Cwd != "" {
+ if !r.HasExtension("container_exec_user_group_cwd") {
+ return nil, fmt.Errorf("The server is missing the required \"container_exec_user_group_cwd\" API extension")
+ }
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("/instances/%s/exec", url.PathEscape(instanceName)), exec, "")
+ if err != nil {
+ return nil, err
+ }
+ opAPI := op.Get()
+
+ // Process additional arguments
+ if args != nil {
+ // Parse the fds
+ fds := map[string]string{}
+
+ value, ok := opAPI.Metadata["fds"]
+ if ok {
+ values := value.(map[string]interface{})
+ for k, v := range values {
+ fds[k] = v.(string)
+ }
+ }
+
+ // Call the control handler with a connection to the control socket
+ if args.Control != nil && fds["control"] != "" {
+ conn, err := r.GetOperationWebsocket(opAPI.ID, fds["control"])
+ if err != nil {
+ return nil, err
+ }
+
+ go args.Control(conn)
+ }
+
+ if exec.Interactive {
+ // Handle interactive sections
+ if args.Stdin != nil && args.Stdout != nil {
+ // Connect to the websocket
+ conn, err := r.GetOperationWebsocket(opAPI.ID, fds["0"])
+ if err != nil {
+ return nil, err
+ }
+
+ // And attach stdin and stdout to it
+ go func() {
+ shared.WebsocketSendStream(conn, args.Stdin, -1)
+ <-shared.WebsocketRecvStream(args.Stdout, conn)
+ conn.Close()
+
+ if args.DataDone != nil {
+ close(args.DataDone)
+ }
+ }()
+ } else {
+ if args.DataDone != nil {
+ close(args.DataDone)
+ }
+ }
+ } else {
+ // Handle non-interactive sessions
+ dones := map[int]chan bool{}
+ conns := []*websocket.Conn{}
+
+ // Handle stdin
+ if fds["0"] != "" {
+ conn, err := r.GetOperationWebsocket(opAPI.ID, fds["0"])
+ if err != nil {
+ return nil, err
+ }
+
+ conns = append(conns, conn)
+ dones[0] = shared.WebsocketSendStream(conn, args.Stdin, -1)
+ }
+
+ // Handle stdout
+ if fds["1"] != "" {
+ conn, err := r.GetOperationWebsocket(opAPI.ID, fds["1"])
+ if err != nil {
+ return nil, err
+ }
+
+ conns = append(conns, conn)
+ dones[1] = shared.WebsocketRecvStream(args.Stdout, conn)
+ }
+
+ // Handle stderr
+ if fds["2"] != "" {
+ conn, err := r.GetOperationWebsocket(opAPI.ID, fds["2"])
+ if err != nil {
+ return nil, err
+ }
+
+ conns = append(conns, conn)
+ dones[2] = shared.WebsocketRecvStream(args.Stderr, conn)
+ }
+
+ // Wait for everything to be done
+ go func() {
+ for i, chDone := range dones {
+ // Skip stdin, dealing with it separately below
+ if i == 0 {
+ continue
+ }
+
+ <-chDone
+ }
+
+ if fds["0"] != "" {
+ if args.Stdin != nil {
+ args.Stdin.Close()
+ }
+
+ // Empty the stdin channel but don't block on it as
+ // stdin may be stuck in Read()
+ go func() {
+ <-dones[0]
+ }()
+ }
+
+ for _, conn := range conns {
+ conn.Close()
+ }
+
+ if args.DataDone != nil {
+ close(args.DataDone)
+ }
+ }()
+ }
+ }
+
+ return op, nil
+}
+
+// GetInstanceFile retrieves the provided path from the instance.
+func (r *ProtocolLXD) GetInstanceFile(instanceName string, path string) (io.ReadCloser, *InstanceFileResponse, error) {
+ // Prepare the HTTP request
+ requestURL, err := shared.URLEncode(
+ fmt.Sprintf("%s/1.0/instances/%s/files", r.httpHost, url.PathEscape(instanceName)),
+ map[string]string{"path": path})
+ if err != nil {
+ return nil, nil, err
+ }
+
+ requestURL, err = r.setQueryAttributes(requestURL)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ req, err := http.NewRequest("GET", requestURL, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // Set the user agent
+ if r.httpUserAgent != "" {
+ req.Header.Set("User-Agent", r.httpUserAgent)
+ }
+
+ // Send the request
+ resp, err := r.do(req)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // Check the return value for a cleaner error
+ if resp.StatusCode != http.StatusOK {
+ _, _, err := lxdParseResponse(resp)
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+
+ // Parse the headers
+ uid, gid, mode, fileType, _ := shared.ParseLXDFileHeaders(resp.Header)
+ fileResp := InstanceFileResponse{
+ UID: uid,
+ GID: gid,
+ Mode: mode,
+ Type: fileType,
+ }
+
+ if fileResp.Type == "directory" {
+ // Decode the response
+ response := api.Response{}
+ decoder := json.NewDecoder(resp.Body)
+
+ err = decoder.Decode(&response)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // Get the file list
+ entries := []string{}
+ err = response.MetadataAsStruct(&entries)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ fileResp.Entries = entries
+
+ return nil, &fileResp, err
+ }
+
+ return resp.Body, &fileResp, err
+}
+
+// CreateInstanceFile tells LXD to create a file in the instance.
+func (r *ProtocolLXD) CreateInstanceFile(instanceName string, path string, args InstanceFileArgs) error {
+ if args.Type == "directory" {
+ if !r.HasExtension("directory_manipulation") {
+ return fmt.Errorf("The server is missing the required \"directory_manipulation\" API extension")
+ }
+ }
+
+ if args.Type == "symlink" {
+ if !r.HasExtension("file_symlinks") {
+ return fmt.Errorf("The server is missing the required \"file_symlinks\" API extension")
+ }
+ }
+
+ if args.WriteMode == "append" {
+ if !r.HasExtension("file_append") {
+ return fmt.Errorf("The server is missing the required \"file_append\" API extension")
+ }
+ }
+
+ // Prepare the HTTP request
+ requestURL := fmt.Sprintf("%s/1.0/instances/%s/files?path=%s", r.httpHost, url.PathEscape(instanceName), url.QueryEscape(path))
+
+ requestURL, err := r.setQueryAttributes(requestURL)
+ if err != nil {
+ return err
+ }
+
+ req, err := http.NewRequest("POST", requestURL, args.Content)
+ if err != nil {
+ return err
+ }
+
+ // Set the user agent
+ if r.httpUserAgent != "" {
+ req.Header.Set("User-Agent", r.httpUserAgent)
+ }
+
+ // Set the various headers
+ if args.UID > -1 {
+ req.Header.Set("X-LXD-uid", fmt.Sprintf("%d", args.UID))
+ }
+
+ if args.GID > -1 {
+ req.Header.Set("X-LXD-gid", fmt.Sprintf("%d", args.GID))
+ }
+
+ if args.Mode > -1 {
+ req.Header.Set("X-LXD-mode", fmt.Sprintf("%04o", args.Mode))
+ }
+
+ if args.Type != "" {
+ req.Header.Set("X-LXD-type", args.Type)
+ }
+
+ if args.WriteMode != "" {
+ req.Header.Set("X-LXD-write", args.WriteMode)
+ }
+
+ // Send the request
+ resp, err := r.do(req)
+ if err != nil {
+ return err
+ }
+
+ // Check the return value for a cleaner error
+ _, _, err = lxdParseResponse(resp)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// DeleteInstanceFile deletes a file in the instance.
+func (r *ProtocolLXD) DeleteInstanceFile(instanceName string, path string) error {
+ if !r.HasExtension("file_delete") {
+ return fmt.Errorf("The server is missing the required \"file_delete\" API extension")
+ }
+
+ // Send the request
+ _, _, err := r.query("DELETE", fmt.Sprintf("/instances/%s/files?path=%s", url.PathEscape(instanceName), url.QueryEscape(path)), nil, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// GetInstanceSnapshotNames returns a list of snapshot names for the instance.
+func (r *ProtocolLXD) GetInstanceSnapshotNames(instanceName string) ([]string, error) {
+ urls := []string{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", fmt.Sprintf("/instances/%s/snapshots", url.PathEscape(instanceName)), nil, "", &urls)
+ if err != nil {
+ return nil, err
+ }
+
+ // Parse it
+ names := []string{}
+ for _, uri := range urls {
+ fields := strings.Split(uri, fmt.Sprintf("/instances/%s/snapshots/", url.PathEscape(instanceName)))
+ names = append(names, fields[len(fields)-1])
+ }
+
+ return names, nil
+}
+
+// GetInstanceSnapshots returns a list of snapshots for the instance.
+func (r *ProtocolLXD) GetInstanceSnapshots(instanceName string) ([]api.InstanceSnapshot, error) {
+ snapshots := []api.InstanceSnapshot{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", fmt.Sprintf("/instances/%s/snapshots?recursion=1", url.PathEscape(instanceName)), nil, "", &snapshots)
+ if err != nil {
+ return nil, err
+ }
+
+ return snapshots, nil
+}
+
+// GetInstanceSnapshot returns a Snapshot struct for the provided instance and snapshot names
+func (r *ProtocolLXD) GetInstanceSnapshot(instanceName string, name string) (*api.InstanceSnapshot, string, error) {
+ snapshot := api.InstanceSnapshot{}
+
+ // Fetch the raw value
+ etag, err := r.queryStruct("GET", fmt.Sprintf("/instances/%s/snapshots/%s", url.PathEscape(instanceName), url.PathEscape(name)), nil, "", &snapshot)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return &snapshot, etag, nil
+}
+
+// CreateInstanceSnapshot requests that LXD creates a new snapshot for the instance.
+func (r *ProtocolLXD) CreateInstanceSnapshot(instanceName string, snapshot api.InstanceSnapshotsPost) (Operation, error) {
+ // Validate the request
+ if snapshot.ExpiresAt != nil && !r.HasExtension("snapshot_expiry_creation") {
+ return nil, fmt.Errorf("The server is missing the required \"snapshot_expiry_creation\" API extension")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("/instances/%s/snapshots", url.PathEscape(instanceName)), snapshot, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// CopyInstanceSnapshot copies a snapshot from a remote server into a new instance. Additional options can be passed using InstanceCopyArgs.
+func (r *ProtocolLXD) CopyInstanceSnapshot(source InstanceServer, instanceName string, snapshot api.InstanceSnapshot, args *InstanceSnapshotCopyArgs) (RemoteOperation, error) {
+ // Backward compatibility (with broken Name field)
+ fields := strings.Split(snapshot.Name, shared.SnapshotDelimiter)
+ cName := instanceName
+ sName := fields[len(fields)-1]
+
+ // Base request
+ req := api.InstancesPost{
+ Name: cName,
+ InstancePut: api.InstancePut{
+ Architecture: snapshot.Architecture,
+ Config: snapshot.Config,
+ Devices: snapshot.Devices,
+ Ephemeral: snapshot.Ephemeral,
+ Profiles: snapshot.Profiles,
+ },
+ }
+
+ if snapshot.Stateful && args.Live {
+ if !r.HasExtension("container_snapshot_stateful_migration") {
+ return nil, fmt.Errorf("The server is missing the required \"container_snapshot_stateful_migration\" API extension")
+ }
+ req.InstancePut.Stateful = snapshot.Stateful
+ req.Source.Live = args.Live
+ }
+ req.Source.BaseImage = snapshot.Config["volatile.base_image"]
+
+ // Process the copy arguments
+ if args != nil {
+ // Sanity checks
+ if shared.StringInSlice(args.Mode, []string{"push", "relay"}) {
+ if !r.HasExtension("container_push") {
+ return nil, fmt.Errorf("The target server is missing the required \"container_push\" API extension")
+ }
+
+ if !source.HasExtension("container_push") {
+ return nil, fmt.Errorf("The source server is missing the required \"container_push\" API extension")
+ }
+ }
+
+ if args.Mode == "push" && !source.HasExtension("container_push_target") {
+ return nil, fmt.Errorf("The source server is missing the required \"container_push_target\" API extension")
+ }
+
+ // Allow overriding the target name
+ if args.Name != "" {
+ req.Name = args.Name
+ }
+ }
+
+ sourceInfo, err := source.GetConnectionInfo()
+ if err != nil {
+ return nil, fmt.Errorf("Failed to get source connection info: %v", err)
+ }
+
+ destInfo, err := r.GetConnectionInfo()
+ if err != nil {
+ return nil, fmt.Errorf("Failed to get destination connection info: %v", err)
+ }
+
+ instance, _, err := source.GetInstance(cName)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to get instance info: %v", err)
+ }
+
+ // Optimization for the local copy case
+ if destInfo.URL == sourceInfo.URL && destInfo.SocketPath == sourceInfo.SocketPath && (!r.IsClustered() || instance.Location == r.clusterTarget || r.HasExtension("cluster_internal_copy")) {
+ // Project handling
+ if destInfo.Project != sourceInfo.Project {
+ if !r.HasExtension("container_copy_project") {
+ return nil, fmt.Errorf("The server is missing the required \"container_copy_project\" API extension")
+ }
+
+ req.Source.Project = sourceInfo.Project
+ }
+
+ // Local copy source fields
+ req.Source.Type = "copy"
+ req.Source.Source = fmt.Sprintf("%s/%s", cName, sName)
+
+ // Copy the instance
+ op, err := r.CreateInstance(req)
+ if err != nil {
+ return nil, err
+ }
+
+ rop := remoteOperation{
+ targetOp: op,
+ chDone: make(chan bool),
+ }
+
+ // Forward targetOp to remote op
+ go func() {
+ rop.err = rop.targetOp.Wait()
+ close(rop.chDone)
+ }()
+
+ return &rop, nil
+ }
+
+ // Source request
+ sourceReq := api.InstanceSnapshotPost{
+ Migration: true,
+ Name: args.Name,
+ }
+ if snapshot.Stateful && args.Live {
+ sourceReq.Live = args.Live
+ }
+
+ // Push mode migration
+ if args != nil && args.Mode == "push" {
+ // Get target server connection information
+ info, err := r.GetConnectionInfo()
+ if err != nil {
+ return nil, err
+ }
+
+ // Create the instance
+ req.Source.Type = "migration"
+ req.Source.Mode = "push"
+
+ op, err := r.CreateInstance(req)
+ if err != nil {
+ return nil, err
+ }
+ opAPI := op.Get()
+
+ targetSecrets := map[string]string{}
+ for k, v := range opAPI.Metadata {
+ targetSecrets[k] = v.(string)
+ }
+
+ // Prepare the source request
+ target := api.InstancePostTarget{}
+ target.Operation = opAPI.ID
+ target.Websockets = targetSecrets
+ target.Certificate = info.Certificate
+ sourceReq.Target = &target
+
+ return r.tryMigrateInstanceSnapshot(source, cName, sName, sourceReq, info.Addresses)
+ }
+
+ // Get source server connection information
+ info, err := source.GetConnectionInfo()
+ if err != nil {
+ return nil, err
+ }
+
+ op, err := source.MigrateInstanceSnapshot(cName, sName, sourceReq)
+ if err != nil {
+ return nil, err
+ }
+ opAPI := op.Get()
+
+ sourceSecrets := map[string]string{}
+ for k, v := range opAPI.Metadata {
+ sourceSecrets[k] = v.(string)
+ }
+
+ // Relay mode migration
+ if args != nil && args.Mode == "relay" {
+ // Push copy source fields
+ req.Source.Type = "migration"
+ req.Source.Mode = "push"
+
+ // Start the process
+ targetOp, err := r.CreateInstance(req)
+ if err != nil {
+ return nil, err
+ }
+ targetOpAPI := targetOp.Get()
+
+ // Extract the websockets
+ targetSecrets := map[string]string{}
+ for k, v := range targetOpAPI.Metadata {
+ targetSecrets[k] = v.(string)
+ }
+
+ // Launch the relay
+ err = r.proxyMigration(targetOp.(*operation), targetSecrets, source, op.(*operation), sourceSecrets)
+ if err != nil {
+ return nil, err
+ }
+
+ // Prepare a tracking operation
+ rop := remoteOperation{
+ targetOp: targetOp,
+ chDone: make(chan bool),
+ }
+
+ // Forward targetOp to remote op
+ go func() {
+ rop.err = rop.targetOp.Wait()
+ close(rop.chDone)
+ }()
+
+ return &rop, nil
+ }
+
+ // Pull mode migration
+ req.Source.Type = "migration"
+ req.Source.Mode = "pull"
+ req.Source.Operation = opAPI.ID
+ req.Source.Websockets = sourceSecrets
+ req.Source.Certificate = info.Certificate
+
+ return r.tryCreateInstance(req, info.Addresses)
+}
+
+// RenameInstanceSnapshot requests that LXD renames the snapshot.
+func (r *ProtocolLXD) RenameInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPost) (Operation, error) {
+ // Sanity check
+ if instance.Migration {
+ return nil, fmt.Errorf("Can't ask for a migration through RenameInstanceSnapshot")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("/instances/%s/snapshots/%s", url.PathEscape(instanceName), url.PathEscape(name)), instance, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+func (r *ProtocolLXD) tryMigrateInstanceSnapshot(source InstanceServer, instanceName string, name string, req api.InstanceSnapshotPost, urls []string) (RemoteOperation, error) {
+ if len(urls) == 0 {
+ return nil, fmt.Errorf("The target server isn't listening on the network")
+ }
+
+ rop := remoteOperation{
+ chDone: make(chan bool),
+ }
+
+ operation := req.Target.Operation
+
+ // Forward targetOp to remote op
+ go func() {
+ success := false
+ errors := map[string]error{}
+ for _, serverURL := range urls {
+ req.Target.Operation = fmt.Sprintf("%s/1.0/operations/%s", serverURL, url.PathEscape(operation))
+
+ op, err := source.MigrateInstanceSnapshot(instanceName, name, req)
+ if err != nil {
+ errors[serverURL] = err
+ continue
+ }
+
+ rop.targetOp = op
+
+ for _, handler := range rop.handlers {
+ rop.targetOp.AddHandler(handler)
+ }
+
+ err = rop.targetOp.Wait()
+ if err != nil {
+ errors[serverURL] = err
+ continue
+ }
+
+ success = true
+ break
+ }
+
+ if !success {
+ rop.err = remoteOperationError("Failed instance migration", errors)
+ }
+
+ close(rop.chDone)
+ }()
+
+ return &rop, nil
+}
+
+// MigrateInstanceSnapshot requests that LXD prepares for a snapshot migration.
+func (r *ProtocolLXD) MigrateInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPost) (Operation, error) {
+ // Sanity check
+ if !instance.Migration {
+ return nil, fmt.Errorf("Can't ask for a rename through MigrateInstanceSnapshot")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("/instances/%s/snapshots/%s", url.PathEscape(instanceName), url.PathEscape(name)), instance, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// DeleteInstanceSnapshot requests that LXD deletes the instance snapshot.
+func (r *ProtocolLXD) DeleteInstanceSnapshot(instanceName string, name string) (Operation, error) {
+ // Send the request
+ op, _, err := r.queryOperation("DELETE", fmt.Sprintf("/instances/%s/snapshots/%s", url.PathEscape(instanceName), url.PathEscape(name)), nil, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// UpdateInstanceSnapshot requests that LXD updates the instance snapshot.
+func (r *ProtocolLXD) UpdateInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPut, ETag string) (Operation, error) {
+ if !r.HasExtension("snapshot_expiry") {
+ return nil, fmt.Errorf("The server is missing the required \"snapshot_expiry\" API extension")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("PUT", fmt.Sprintf("/instances/%s/snapshots/%s",
+ url.PathEscape(instanceName), url.PathEscape(name)), instance, ETag)
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// GetInstanceState returns a InstanceState entry for the provided instance name.
+func (r *ProtocolLXD) GetInstanceState(name string) (*api.InstanceState, string, error) {
+ state := api.InstanceState{}
+
+ // Fetch the raw value
+ etag, err := r.queryStruct("GET", fmt.Sprintf("/instances/%s/state", url.PathEscape(name)), nil, "", &state)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return &state, etag, nil
+}
+
+// UpdateInstanceState updates the instance to match the requested state.
+func (r *ProtocolLXD) UpdateInstanceState(name string, state api.InstanceStatePut, ETag string) (Operation, error) {
+ // Send the request
+ op, _, err := r.queryOperation("PUT", fmt.Sprintf("/instances/%s/state", url.PathEscape(name)), state, ETag)
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// GetInstanceLogfiles returns a list of logfiles for the instance.
+func (r *ProtocolLXD) GetInstanceLogfiles(name string) ([]string, error) {
+ urls := []string{}
+
+ // Fetch the raw value
+ _, err := r.queryStruct("GET", fmt.Sprintf("/instances/%s/logs", url.PathEscape(name)), nil, "", &urls)
+ if err != nil {
+ return nil, err
+ }
+
+ // Parse it
+ logfiles := []string{}
+ for _, uri := range logfiles {
+ fields := strings.Split(uri, fmt.Sprintf("/instances/%s/logs/", url.PathEscape(name)))
+ logfiles = append(logfiles, fields[len(fields)-1])
+ }
+
+ return logfiles, nil
+}
+
+// GetInstanceLogfile returns the content of the requested logfile.
+//
+// Note that it's the caller's responsibility to close the returned ReadCloser
+func (r *ProtocolLXD) GetInstanceLogfile(name string, filename string) (io.ReadCloser, error) {
+ // Prepare the HTTP request
+ url := fmt.Sprintf("%s/1.0/instances/%s/logs/%s", r.httpHost, url.PathEscape(name), url.PathEscape(filename))
+
+ url, err := r.setQueryAttributes(url)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ // Set the user agent
+ if r.httpUserAgent != "" {
+ req.Header.Set("User-Agent", r.httpUserAgent)
+ }
+
+ // Send the request
+ resp, err := r.do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ // Check the return value for a cleaner error
+ if resp.StatusCode != http.StatusOK {
+ _, _, err := lxdParseResponse(resp)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return resp.Body, err
+}
+
+// DeleteInstanceLogfile deletes the requested logfile.
+func (r *ProtocolLXD) DeleteInstanceLogfile(name string, filename string) error {
+ // Send the request
+ _, _, err := r.query("DELETE", fmt.Sprintf("/instances/%s/logs/%s", url.PathEscape(name), url.PathEscape(filename)), nil, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// GetInstanceMetadata returns instance metadata.
+func (r *ProtocolLXD) GetInstanceMetadata(name string) (*api.ImageMetadata, string, error) {
+ if !r.HasExtension("container_edit_metadata") {
+ return nil, "", fmt.Errorf("The server is missing the required \"container_edit_metadata\" API extension")
+ }
+
+ metadata := api.ImageMetadata{}
+
+ url := fmt.Sprintf("/instances/%s/metadata", url.PathEscape(name))
+ etag, err := r.queryStruct("GET", url, nil, "", &metadata)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return &metadata, etag, err
+}
+
+// SetInstanceMetadata sets the content of the instance metadata file.
+func (r *ProtocolLXD) SetInstanceMetadata(name string, metadata api.ImageMetadata, ETag string) error {
+ if !r.HasExtension("container_edit_metadata") {
+ return fmt.Errorf("The server is missing the required \"container_edit_metadata\" API extension")
+ }
+
+ url := fmt.Sprintf("/instances/%s/metadata", url.PathEscape(name))
+ _, _, err := r.query("PUT", url, metadata, ETag)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// GetInstanceTemplateFiles returns the list of names of template files for a instance.
+func (r *ProtocolLXD) GetInstanceTemplateFiles(instanceName string) ([]string, error) {
+ if !r.HasExtension("container_edit_metadata") {
+ return nil, fmt.Errorf("The server is missing the required \"container_edit_metadata\" API extension")
+ }
+
+ templates := []string{}
+
+ url := fmt.Sprintf("/instances/%s/metadata/templates", url.PathEscape(instanceName))
+ _, err := r.queryStruct("GET", url, nil, "", &templates)
+ if err != nil {
+ return nil, err
+ }
+
+ return templates, nil
+}
+
+// GetInstanceTemplateFile returns the content of a template file for a instance.
+func (r *ProtocolLXD) GetInstanceTemplateFile(instanceName string, templateName string) (io.ReadCloser, error) {
+ if !r.HasExtension("container_edit_metadata") {
+ return nil, fmt.Errorf("The server is missing the required \"container_edit_metadata\" API extension")
+ }
+
+ url := fmt.Sprintf("%s/1.0/instances/%s/metadata/templates?path=%s", r.httpHost, url.PathEscape(instanceName), url.QueryEscape(templateName))
+
+ url, err := r.setQueryAttributes(url)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ // Set the user agent
+ if r.httpUserAgent != "" {
+ req.Header.Set("User-Agent", r.httpUserAgent)
+ }
+
+ // Send the request
+ resp, err := r.do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ // Check the return value for a cleaner error
+ if resp.StatusCode != http.StatusOK {
+ _, _, err := lxdParseResponse(resp)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return resp.Body, err
+}
+
+// CreateInstanceTemplateFile creates an a template for a instance.
+func (r *ProtocolLXD) CreateInstanceTemplateFile(instanceName string, templateName string, content io.ReadSeeker) error {
+ return r.setInstanceTemplateFile(instanceName, templateName, content, "POST")
+}
+
+// UpdateInstanceTemplateFile updates the content for a instance template file.
+func (r *ProtocolLXD) UpdateInstanceTemplateFile(instanceName string, templateName string, content io.ReadSeeker) error {
+ return r.setInstanceTemplateFile(instanceName, templateName, content, "PUT")
+}
+
+func (r *ProtocolLXD) setInstanceTemplateFile(instanceName string, templateName string, content io.ReadSeeker, httpMethod string) error {
+ if !r.HasExtension("container_edit_metadata") {
+ return fmt.Errorf("The server is missing the required \"container_edit_metadata\" API extension")
+ }
+
+ url := fmt.Sprintf("%s/1.0/instances/%s/metadata/templates?path=%s", r.httpHost, url.PathEscape(instanceName), url.QueryEscape(templateName))
+
+ url, err := r.setQueryAttributes(url)
+ if err != nil {
+ return err
+ }
+
+ req, err := http.NewRequest(httpMethod, url, content)
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Content-Type", "application/octet-stream")
+
+ // Set the user agent
+ if r.httpUserAgent != "" {
+ req.Header.Set("User-Agent", r.httpUserAgent)
+ }
+
+ // Send the request
+ resp, err := r.do(req)
+ // Check the return value for a cleaner error
+ if resp.StatusCode != http.StatusOK {
+ _, _, err := lxdParseResponse(resp)
+ if err != nil {
+ return err
+ }
+ }
+ return err
+}
+
+// DeleteInstanceTemplateFile deletes a template file for a instance.
+func (r *ProtocolLXD) DeleteInstanceTemplateFile(name string, templateName string) error {
+ if !r.HasExtension("container_edit_metadata") {
+ return fmt.Errorf("The server is missing the required \"container_edit_metadata\" API extension")
+ }
+ _, _, err := r.query("DELETE", fmt.Sprintf("/instances/%s/metadata/templates?path=%s", url.PathEscape(name), url.QueryEscape(templateName)), nil, "")
+ return err
+}
+
+// ConsoleInstance requests that LXD attaches to the console device of a instance.
+func (r *ProtocolLXD) ConsoleInstance(instanceName string, console api.InstanceConsolePost, args *InstanceConsoleArgs) (Operation, error) {
+ if !r.HasExtension("console") {
+ return nil, fmt.Errorf("The server is missing the required \"console\" API extension")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("/instances/%s/console", url.PathEscape(instanceName)), console, "")
+ if err != nil {
+ return nil, err
+ }
+ opAPI := op.Get()
+
+ if args == nil || args.Terminal == nil {
+ return nil, fmt.Errorf("A terminal must be set")
+ }
+
+ if args.Control == nil {
+ return nil, fmt.Errorf("A control channel must be set")
+ }
+
+ // Parse the fds
+ fds := map[string]string{}
+
+ value, ok := opAPI.Metadata["fds"]
+ if ok {
+ values := value.(map[string]interface{})
+ for k, v := range values {
+ fds[k] = v.(string)
+ }
+ }
+
+ var controlConn *websocket.Conn
+ // Call the control handler with a connection to the control socket
+ if fds["control"] == "" {
+ return nil, fmt.Errorf("Did not receive a file descriptor for the control channel")
+ }
+
+ controlConn, err = r.GetOperationWebsocket(opAPI.ID, fds["control"])
+ if err != nil {
+ return nil, err
+ }
+
+ go args.Control(controlConn)
+
+ // Connect to the websocket
+ conn, err := r.GetOperationWebsocket(opAPI.ID, fds["0"])
+ if err != nil {
+ return nil, err
+ }
+
+ // Detach from console.
+ go func(consoleDisconnect <-chan bool) {
+ <-consoleDisconnect
+ msg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "Detaching from console")
+ // We don't care if this fails. This is just for convenience.
+ controlConn.WriteMessage(websocket.CloseMessage, msg)
+ controlConn.Close()
+ }(args.ConsoleDisconnect)
+
+ // And attach stdin and stdout to it
+ go func() {
+ shared.WebsocketSendStream(conn, args.Terminal, -1)
+ <-shared.WebsocketRecvStream(args.Terminal, conn)
+ conn.Close()
+ }()
+
+ return op, nil
+}
+
+// GetInstanceConsoleLog requests that LXD attaches to the console device of a instance.
+//
+// Note that it's the caller's responsibility to close the returned ReadCloser
+func (r *ProtocolLXD) GetInstanceConsoleLog(instanceName string, args *InstanceConsoleLogArgs) (io.ReadCloser, error) {
+ if !r.HasExtension("console") {
+ return nil, fmt.Errorf("The server is missing the required \"console\" API extension")
+ }
+
+ // Prepare the HTTP request
+ url := fmt.Sprintf("%s/1.0/instances/%s/console", r.httpHost, url.PathEscape(instanceName))
+
+ url, err := r.setQueryAttributes(url)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ // Set the user agent
+ if r.httpUserAgent != "" {
+ req.Header.Set("User-Agent", r.httpUserAgent)
+ }
+
+ // Send the request
+ resp, err := r.do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ // Check the return value for a cleaner error
+ if resp.StatusCode != http.StatusOK {
+ _, _, err := lxdParseResponse(resp)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return resp.Body, err
+}
+
+// DeleteInstanceConsoleLog deletes the requested instance's console log.
+func (r *ProtocolLXD) DeleteInstanceConsoleLog(instanceName string, args *InstanceConsoleLogArgs) error {
+ if !r.HasExtension("console") {
+ return fmt.Errorf("The server is missing the required \"console\" API extension")
+ }
+
+ // Send the request
+ _, _, err := r.query("DELETE", fmt.Sprintf("/instances/%s/console", url.PathEscape(instanceName)), nil, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// GetInstanceBackupNames returns a list of backup names for the instance.
+func (r *ProtocolLXD) GetInstanceBackupNames(instanceName string) ([]string, error) {
+ if !r.HasExtension("container_backup") {
+ return nil, fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+ }
+
+ // Fetch the raw value
+ urls := []string{}
+ _, err := r.queryStruct("GET", fmt.Sprintf("/instances/%s/backups",
+ url.PathEscape(instanceName)), nil, "", &urls)
+ if err != nil {
+ return nil, err
+ }
+
+ // Parse it
+ names := []string{}
+ for _, uri := range urls {
+ fields := strings.Split(uri, fmt.Sprintf("/instances/%s/backups/",
+ url.PathEscape(instanceName)))
+ names = append(names, fields[len(fields)-1])
+ }
+
+ return names, nil
+}
+
+// GetInstanceBackups returns a list of backups for the instance.
+func (r *ProtocolLXD) GetInstanceBackups(instanceName string) ([]api.InstanceBackup, error) {
+ if !r.HasExtension("container_backup") {
+ return nil, fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+ }
+
+ // Fetch the raw value
+ backups := []api.InstanceBackup{}
+
+ _, err := r.queryStruct("GET", fmt.Sprintf("/instances/%s/backups?recursion=1", url.PathEscape(instanceName)), nil, "", &backups)
+ if err != nil {
+ return nil, err
+ }
+
+ return backups, nil
+}
+
+// GetInstanceBackup returns a Backup struct for the provided instance and backup names.
+func (r *ProtocolLXD) GetInstanceBackup(instanceName string, name string) (*api.InstanceBackup, string, error) {
+ if !r.HasExtension("container_backup") {
+ return nil, "", fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+ }
+
+ // Fetch the raw value
+ backup := api.InstanceBackup{}
+ etag, err := r.queryStruct("GET", fmt.Sprintf("/instances/%s/backups/%s", url.PathEscape(instanceName), url.PathEscape(name)), nil, "", &backup)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return &backup, etag, nil
+}
+
+// CreateInstanceBackup requests that LXD creates a new backup for the instance.
+func (r *ProtocolLXD) CreateInstanceBackup(instanceName string, backup api.InstanceBackupsPost) (Operation, error) {
+ if !r.HasExtension("container_backup") {
+ return nil, fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("/instances/%s/backups",
+ url.PathEscape(instanceName)), backup, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// RenameInstanceBackup requests that LXD renames the backup.
+func (r *ProtocolLXD) RenameInstanceBackup(instanceName string, name string, backup api.InstanceBackupPost) (Operation, error) {
+ if !r.HasExtension("container_backup") {
+ return nil, fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("/instances/%s/backups/%s",
+ url.PathEscape(instanceName), url.PathEscape(name)), backup, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// DeleteInstanceBackup requests that LXD deletes the instance backup.
+func (r *ProtocolLXD) DeleteInstanceBackup(instanceName string, name string) (Operation, error) {
+ if !r.HasExtension("container_backup") {
+ return nil, fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("DELETE", fmt.Sprintf("/instances/%s/backups/%s",
+ url.PathEscape(instanceName), url.PathEscape(name)), nil, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// GetInstanceBackupFile requests the instance backup content.
+func (r *ProtocolLXD) GetInstanceBackupFile(instanceName string, name string, req *BackupFileRequest) (*BackupFileResponse, error) {
+ if !r.HasExtension("container_backup") {
+ return nil, fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+ }
+
+ // Build the URL
+ uri := fmt.Sprintf("%s/1.0/instances/%s/backups/%s/export", r.httpHost,
+ url.PathEscape(instanceName), url.PathEscape(name))
+ if r.project != "" {
+ uri += fmt.Sprintf("?project=%s", url.QueryEscape(r.project))
+ }
+
+ // Prepare the download request
+ request, err := http.NewRequest("GET", uri, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ if r.httpUserAgent != "" {
+ request.Header.Set("User-Agent", r.httpUserAgent)
+ }
+
+ // Start the request
+ response, doneCh, err := cancel.CancelableDownload(req.Canceler, r.http, request)
+ if err != nil {
+ return nil, err
+ }
+ defer response.Body.Close()
+ defer close(doneCh)
+
+ if response.StatusCode != http.StatusOK {
+ _, _, err := lxdParseResponse(response)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // Handle the data
+ body := response.Body
+ if req.ProgressHandler != nil {
+ body = &ioprogress.ProgressReader{
+ ReadCloser: response.Body,
+ Tracker: &ioprogress.ProgressTracker{
+ Length: response.ContentLength,
+ Handler: func(percent int64, speed int64) {
+ req.ProgressHandler(ioprogress.ProgressData{Text: fmt.Sprintf("%d%% (%s/s)", percent, units.GetByteSizeString(speed, 2))})
+ },
+ },
+ }
+ }
+
+ size, err := io.Copy(req.BackupFile, body)
+ if err != nil {
+ return nil, err
+ }
+
+ resp := BackupFileResponse{}
+ resp.Size = size
+
+ return &resp, nil
+}
More information about the lxc-devel
mailing list