The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/lxd/pull/6194
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) === This PR continues what was started in https://github.com/lxc/lxd/pull/6193 by adding a full set of instance management functions to the `InstanceServer` interface. It also adds the implementations of those functions that use the `/1.0/instances` endpoints.
From d7d30a08104061b1f74a51c92dcfcf184cd0c899 Mon Sep 17 00:00:00 2001 From: Thomas Parrott <thomas.parr...@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.parr...@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.parr...@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.parr...@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.parr...@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.parr...@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.parr...@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.parr...@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.parr...@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.parr...@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.parr...@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.parr...@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 +}
_______________________________________________ lxc-devel mailing list lxc-devel@lists.linuxcontainers.org http://lists.linuxcontainers.org/listinfo/lxc-devel