[lxc-devel] [lxd/master] lxc: Switches to use Instance functions of client package
tomponline on Github
lxc-bot at linuxcontainers.org
Fri Sep 13 08:55:44 UTC 2019
A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 301 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20190913/f6dfa5f7/attachment-0001.bin>
-------------- next part --------------
From c222666f66fd129e49a92f19dda1365a2c9392f0 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 1/3] client/interfaces: Populates InstanceServer with rest of
functions
Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
client/interfaces.go | 57 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 57 insertions(+)
diff --git a/client/interfaces.go b/client/interfaces.go
index 2fe2e1aadb..a7179830d3 100644
--- a/client/interfaces.go
+++ b/client/interfaces.go
@@ -136,6 +136,63 @@ type InstanceServer interface {
UpdateContainerTemplateFile(containerName string, templateName string, content io.ReadSeeker) (err error)
DeleteContainerTemplateFile(name string, templateName string) (err error)
+ // Instance functions.
+ GetInstanceNames(instanceType api.InstanceType) (names []string, err error)
+ GetInstances(instanceType api.InstanceType) (instances []api.Instance, err error)
+ GetInstancesFull(instanceType api.InstanceType) (instances []api.InstanceFull, err error)
+ GetInstance(name string) (instance *api.Instance, ETag string, err error)
+ CreateInstance(instance api.InstancesPost) (op Operation, err error)
+ CreateInstanceFromImage(source ImageServer, image api.Image, imgcontainer api.InstancesPost) (op RemoteOperation, err error)
+ CopyInstance(source InstanceServer, instance api.Instance, args *ContainerCopyArgs) (op RemoteOperation, err error)
+ UpdateInstance(name string, instance api.InstancePut, ETag string) (op Operation, err error)
+ RenameInstance(name string, instance api.InstancePost) (op Operation, err error)
+ MigrateInstance(name string, instance api.InstancePost) (op Operation, err error)
+ DeleteInstance(name string) (op Operation, err error)
+
+ ExecInstance(instanceName string, exec api.InstanceExecPost, args *ContainerExecArgs) (op Operation, err error)
+ ConsoleInstance(instanceName string, console api.InstanceConsolePost, args *ContainerConsoleArgs) (op Operation, err error)
+ GetInstanceConsoleLog(instanceName string, args *ContainerConsoleLogArgs) (content io.ReadCloser, err error)
+ DeleteInstanceConsoleLog(instanceName string, args *ContainerConsoleLogArgs) (err error)
+
+ GetInstanceFile(instanceName string, path string) (content io.ReadCloser, resp *ContainerFileResponse, err error)
+ CreateInstanceFile(instanceName string, path string, args ContainerFileArgs) (err error)
+ DeleteInstanceFile(instanceName string, path string) (err error)
+
+ GetInstanceSnapshotNames(instanceName string) (names []string, err error)
+ GetInstanceSnapshots(instanceName string) (snapshots []api.InstanceSnapshot, err error)
+ GetInstanceSnapshot(instanceName string, name string) (snapshot *api.InstanceSnapshot, ETag string, err error)
+ CreateInstanceSnapshot(instanceName string, snapshot api.InstanceSnapshotsPost) (op Operation, err error)
+ CopyInstanceSnapshot(source InstanceServer, instanceName string, snapshot api.InstanceSnapshot, args *ContainerSnapshotCopyArgs) (op RemoteOperation, err error)
+ RenameInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPost) (op Operation, err error)
+ MigrateInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPost) (op Operation, err error)
+ DeleteInstanceSnapshot(instanceName string, name string) (op Operation, err error)
+ UpdateInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPut, ETag string) (op Operation, err error)
+
+ GetInstanceBackupNames(instanceName string) (names []string, err error)
+ GetInstanceBackups(instanceName string) (backups []api.InstanceBackup, err error)
+ GetInstanceBackup(instanceName string, name string) (backup *api.InstanceBackup, ETag string, err error)
+ CreateInstanceBackup(instanceName string, backup api.InstanceBackupsPost) (op Operation, err error)
+ RenameInstanceBackup(instanceName string, name string, backup api.InstanceBackupPost) (op Operation, err error)
+ DeleteInstanceBackup(instanceName string, name string) (op Operation, err error)
+ GetInstanceBackupFile(instanceName string, name string, req *BackupFileRequest) (resp *BackupFileResponse, err error)
+ CreateInstanceFromBackup(args ContainerBackupArgs) (op Operation, err error)
+
+ GetInstanceState(name string) (state *api.InstanceState, ETag string, err error)
+ UpdateInstanceState(name string, state api.InstanceStatePut, ETag string) (op Operation, err error)
+
+ GetInstanceLogfiles(name string) (logfiles []string, err error)
+ GetInstanceLogfile(name string, filename string) (content io.ReadCloser, err error)
+ DeleteInstanceLogfile(name string, filename string) (err error)
+
+ GetInstanceMetadata(name string) (metadata *api.ImageMetadata, ETag string, err error)
+ SetInstanceMetadata(name string, metadata api.ImageMetadata, ETag string) (err error)
+
+ GetInstanceTemplateFiles(instanceName string) (templates []string, err error)
+ GetInstanceTemplateFile(instanceName string, templateName string) (content io.ReadCloser, err error)
+ CreateInstanceTemplateFile(instanceName string, templateName string, content io.ReadSeeker) (err error)
+ UpdateInstanceTemplateFile(instanceName string, templateName string, content io.ReadSeeker) (err error)
+ DeleteInstanceTemplateFile(name string, templateName string) (err error)
+
// Event handling functions
GetEvents() (listener *EventListener, err error)
From 30b857bdf6717cf8d920818254624250cfd41d81 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Thu, 12 Sep 2019 15:50:34 +0100
Subject: [PATCH 2/3] client/lxd/instances: Adds instance related functions
These functions use the /1.0/instances endpoints on the server.
Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
client/lxd_instances.go | 2154 +++++++++++++++++++++++++++++++++++++++
1 file changed, 2154 insertions(+)
create mode 100644 client/lxd_instances.go
diff --git a/client/lxd_instances.go b/client/lxd_instances.go
new file mode 100644
index 0000000000..e2345814d8
--- /dev/null
+++ b/client/lxd_instances.go
@@ -0,0 +1,2154 @@
+package lxd
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+
+ "github.com/gorilla/websocket"
+
+ "github.com/lxc/lxd/shared"
+ "github.com/lxc/lxd/shared/api"
+ "github.com/lxc/lxd/shared/cancel"
+ "github.com/lxc/lxd/shared/ioprogress"
+ "github.com/lxc/lxd/shared/units"
+)
+
+// Instance handling functions.
+
+// instanceTypeToPath converts the instance type to a URL path prefix and query string values.
+// If the remote server doesn't have the instances extension then the /containers endpoint is used
+// as long as the requested instanceType is any or container.
+func (r *ProtocolLXD) instanceTypeToPath(instanceType api.InstanceType) (string, url.Values, error) {
+ v := url.Values{}
+
+ // If the remote server doesn't support instances extension, check that only containers
+ // or any type has been requested and then fallback to using the old /containers endpoint.
+ if !r.HasExtension("instances") {
+ if instanceType == api.InstanceTypeContainer || instanceType == api.InstanceTypeAny {
+ return "/containers", v, nil
+ }
+
+ return "", v, fmt.Errorf("Requested instance type not supported by server")
+ }
+
+ // If a specific instance type has been requested, add the instance-type filter parameter
+ // to the returned URL values so that it can be used in the final URL if needed to filter
+ // the result set being returned.
+ if instanceType != api.InstanceTypeAny {
+ v.Set("instance-type", string(instanceType))
+ }
+
+ return "/instances", v, nil
+}
+
+// GetInstanceNames returns a list of instance names.
+func (r *ProtocolLXD) GetInstanceNames(instanceType api.InstanceType) ([]string, error) {
+ urls := []string{}
+
+ path, v, err := r.instanceTypeToPath(instanceType)
+ if err != nil {
+ return nil, err
+ }
+
+ // Fetch the raw value
+ _, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &urls)
+ if err != nil {
+ return nil, err
+ }
+
+ // Parse it
+ names := []string{}
+ prefix := path + "/"
+ for _, url := range urls {
+ fields := strings.Split(url, prefix)
+ names = append(names, fields[len(fields)-1])
+ }
+
+ return names, nil
+}
+
+// GetInstances returns a list of instances.
+func (r *ProtocolLXD) GetInstances(instanceType api.InstanceType) ([]api.Instance, error) {
+ instances := []api.Instance{}
+
+ path, v, err := r.instanceTypeToPath(instanceType)
+ if err != nil {
+ return nil, err
+ }
+
+ v.Set("recursion", "1")
+
+ // Fetch the raw value
+ _, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &instances)
+ if err != nil {
+ return nil, err
+ }
+
+ return instances, nil
+}
+
+// GetInstancesFull returns a list of instances including snapshots, backups and state.
+func (r *ProtocolLXD) GetInstancesFull(instanceType api.InstanceType) ([]api.InstanceFull, error) {
+ instances := []api.InstanceFull{}
+
+ path, v, err := r.instanceTypeToPath(instanceType)
+ if err != nil {
+ return nil, err
+ }
+
+ v.Set("recursion", "2")
+
+ if !r.HasExtension("container_full") {
+ return nil, fmt.Errorf("The server is missing the required \"container_full\" API extension")
+ }
+
+ // Fetch the raw value
+ _, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &instances)
+ if err != nil {
+ return nil, err
+ }
+
+ return instances, nil
+}
+
+// GetInstance returns the instance entry for the provided name.
+func (r *ProtocolLXD) GetInstance(name string) (*api.Instance, string, error) {
+ instance := api.Instance{}
+
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, "", err
+ }
+
+ // Fetch the raw value
+ etag, err := r.queryStruct("GET", fmt.Sprintf("%s/%s", path, url.PathEscape(name)), nil, "", &instance)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return &instance, etag, nil
+}
+
+// CreateInstanceFromBackup is a convenience function to make it easier to
+// create a instance from a backup
+func (r *ProtocolLXD) CreateInstanceFromBackup(args ContainerBackupArgs) (Operation, error) {
+ if !r.HasExtension("container_backup") {
+ return nil, fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+ }
+
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ if args.PoolName == "" {
+ // Send the request
+ op, _, err := r.queryOperation("POST", path, args.BackupFile, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+ }
+
+ if !r.HasExtension("container_backup_override_pool") {
+ return nil, fmt.Errorf("The server is missing the required \"container_backup_override_pool\" API extension")
+ }
+
+ // Prepare the HTTP request
+ reqURL, err := r.setQueryAttributes(fmt.Sprintf("%s/1.0%s", r.httpHost, path))
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("POST", reqURL, args.BackupFile)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Set("Content-Type", "application/octet-stream")
+ req.Header.Set("X-LXD-pool", args.PoolName)
+
+ // Set the user agent
+ if r.httpUserAgent != "" {
+ req.Header.Set("User-Agent", r.httpUserAgent)
+ }
+
+ // Send the request
+ resp, err := r.do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ // Handle errors
+ response, _, err := lxdParseResponse(resp)
+ if err != nil {
+ return nil, err
+ }
+
+ // Get to the operation
+ respOperation, err := response.MetadataAsOperation()
+ if err != nil {
+ return nil, err
+ }
+
+ // Setup an Operation wrapper
+ op := operation{
+ Operation: *respOperation,
+ r: r,
+ chActive: make(chan bool),
+ }
+
+ return &op, nil
+}
+
+// CreateInstance requests that LXD creates a new instance.
+func (r *ProtocolLXD) CreateInstance(instance api.InstancesPost) (Operation, error) {
+ path, _, err := r.instanceTypeToPath(instance.Type)
+ if err != nil {
+ return nil, err
+ }
+
+ if instance.Source.ContainerOnly {
+ if !r.HasExtension("container_only_migration") {
+ return nil, fmt.Errorf("The server is missing the required \"container_only_migration\" API extension")
+ }
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", path, instance, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+func (r *ProtocolLXD) tryCreateInstance(req api.InstancesPost, urls []string) (RemoteOperation, error) {
+ if len(urls) == 0 {
+ return nil, fmt.Errorf("The source server isn't listening on the network")
+ }
+
+ rop := remoteOperation{
+ chDone: make(chan bool),
+ }
+
+ operation := req.Source.Operation
+
+ // Forward targetOp to remote op
+ go func() {
+ success := false
+ errors := map[string]error{}
+ for _, serverURL := range urls {
+ if operation == "" {
+ req.Source.Server = serverURL
+ } else {
+ req.Source.Operation = fmt.Sprintf("%s/1.0/operations/%s", serverURL, url.PathEscape(operation))
+ }
+
+ op, err := r.CreateInstance(req)
+ if err != nil {
+ errors[serverURL] = err
+ continue
+ }
+
+ rop.targetOp = op
+
+ for _, handler := range rop.handlers {
+ rop.targetOp.AddHandler(handler)
+ }
+
+ err = rop.targetOp.Wait()
+ if err != nil {
+ errors[serverURL] = err
+ continue
+ }
+
+ success = true
+ break
+ }
+
+ if !success {
+ rop.err = remoteOperationError("Failed instance creation", errors)
+ }
+
+ close(rop.chDone)
+ }()
+
+ return &rop, nil
+}
+
+// CreateInstanceFromImage is a convenience function to make it easier to create a instance from an existing image.
+func (r *ProtocolLXD) CreateInstanceFromImage(source ImageServer, image api.Image, req api.InstancesPost) (RemoteOperation, error) {
+ // Set the minimal source fields
+ req.Source.Type = "image"
+
+ // Optimization for the local image case
+ if r == source {
+ // Always use fingerprints for local case
+ req.Source.Fingerprint = image.Fingerprint
+ req.Source.Alias = ""
+
+ op, err := r.CreateInstance(req)
+ if err != nil {
+ return nil, err
+ }
+
+ rop := remoteOperation{
+ targetOp: op,
+ chDone: make(chan bool),
+ }
+
+ // Forward targetOp to remote op
+ go func() {
+ rop.err = rop.targetOp.Wait()
+ close(rop.chDone)
+ }()
+
+ return &rop, nil
+ }
+
+ // Minimal source fields for remote image
+ req.Source.Mode = "pull"
+
+ // If we have an alias and the image is public, use that
+ if req.Source.Alias != "" && image.Public {
+ req.Source.Fingerprint = ""
+ } else {
+ req.Source.Fingerprint = image.Fingerprint
+ req.Source.Alias = ""
+ }
+
+ // Get source server connection information
+ info, err := source.GetConnectionInfo()
+ if err != nil {
+ return nil, err
+ }
+
+ req.Source.Protocol = info.Protocol
+ req.Source.Certificate = info.Certificate
+
+ // Generate secret token if needed
+ if !image.Public {
+ secret, err := source.GetImageSecret(image.Fingerprint)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Source.Secret = secret
+ }
+
+ return r.tryCreateInstance(req, info.Addresses)
+}
+
+// CopyInstance copies a instance from a remote server. Additional options can be passed using InstanceCopyArgs.
+func (r *ProtocolLXD) CopyInstance(source InstanceServer, instance api.Instance, args *ContainerCopyArgs) (RemoteOperation, error) {
+ // Base request
+ req := api.InstancesPost{
+ Name: instance.Name,
+ InstancePut: instance.Writable(),
+ }
+ req.Source.BaseImage = instance.Config["volatile.base_image"]
+
+ // Process the copy arguments
+ if args != nil {
+ // Sanity checks
+ if args.ContainerOnly {
+ if !r.HasExtension("container_only_migration") {
+ return nil, fmt.Errorf("The target server is missing the required \"container_only_migration\" API extension")
+ }
+
+ if !source.HasExtension("container_only_migration") {
+ return nil, fmt.Errorf("The source server is missing the required \"container_only_migration\" API extension")
+ }
+ }
+
+ if shared.StringInSlice(args.Mode, []string{"push", "relay"}) {
+ if !r.HasExtension("container_push") {
+ return nil, fmt.Errorf("The target server is missing the required \"container_push\" API extension")
+ }
+
+ if !source.HasExtension("container_push") {
+ return nil, fmt.Errorf("The source server is missing the required \"container_push\" API extension")
+ }
+ }
+
+ if args.Mode == "push" && !source.HasExtension("container_push_target") {
+ return nil, fmt.Errorf("The source server is missing the required \"container_push_target\" API extension")
+ }
+
+ if args.Refresh {
+ if !r.HasExtension("container_incremental_copy") {
+ return nil, fmt.Errorf("The target server is missing the required \"container_incremental_copy\" API extension")
+ }
+
+ if !source.HasExtension("container_incremental_copy") {
+ return nil, fmt.Errorf("The source server is missing the required \"container_incremental_copy\" API extension")
+ }
+ }
+
+ // Allow overriding the target name
+ if args.Name != "" {
+ req.Name = args.Name
+ }
+
+ req.Source.Live = args.Live
+ req.Source.ContainerOnly = args.ContainerOnly
+ req.Source.Refresh = args.Refresh
+ }
+
+ if req.Source.Live {
+ req.Source.Live = instance.StatusCode == api.Running
+ }
+
+ sourceInfo, err := source.GetConnectionInfo()
+ if err != nil {
+ return nil, fmt.Errorf("Failed to get source connection info: %v", err)
+ }
+
+ destInfo, err := r.GetConnectionInfo()
+ if err != nil {
+ return nil, fmt.Errorf("Failed to get destination connection info: %v", err)
+ }
+
+ // Optimization for the local copy case
+ if destInfo.URL == sourceInfo.URL && destInfo.SocketPath == sourceInfo.SocketPath && (!r.IsClustered() || instance.Location == r.clusterTarget || r.HasExtension("cluster_internal_copy")) {
+ // Project handling
+ if destInfo.Project != sourceInfo.Project {
+ if !r.HasExtension("container_copy_project") {
+ return nil, fmt.Errorf("The server is missing the required \"container_copy_project\" API extension")
+ }
+
+ req.Source.Project = sourceInfo.Project
+ }
+
+ // Local copy source fields
+ req.Source.Type = "copy"
+ req.Source.Source = instance.Name
+
+ // Copy the instance
+ op, err := r.CreateInstance(req)
+ if err != nil {
+ return nil, err
+ }
+
+ rop := remoteOperation{
+ targetOp: op,
+ chDone: make(chan bool),
+ }
+
+ // Forward targetOp to remote op
+ go func() {
+ rop.err = rop.targetOp.Wait()
+ close(rop.chDone)
+ }()
+
+ return &rop, nil
+ }
+
+ // Source request
+ sourceReq := api.InstancePost{
+ Migration: true,
+ Live: req.Source.Live,
+ ContainerOnly: req.Source.ContainerOnly,
+ }
+
+ // Push mode migration
+ if args != nil && args.Mode == "push" {
+ // Get target server connection information
+ info, err := r.GetConnectionInfo()
+ if err != nil {
+ return nil, err
+ }
+
+ // Create the instance
+ req.Source.Type = "migration"
+ req.Source.Mode = "push"
+ req.Source.Refresh = args.Refresh
+
+ op, err := r.CreateInstance(req)
+ if err != nil {
+ return nil, err
+ }
+
+ opAPI := op.Get()
+
+ targetSecrets := map[string]string{}
+ for k, v := range opAPI.Metadata {
+ targetSecrets[k] = v.(string)
+ }
+
+ // Prepare the source request
+ target := api.InstancePostTarget{}
+ target.Operation = opAPI.ID
+ target.Websockets = targetSecrets
+ target.Certificate = info.Certificate
+ sourceReq.Target = &target
+
+ return r.tryMigrateInstance(source, instance.Name, sourceReq, info.Addresses)
+ }
+
+ // Get source server connection information
+ info, err := source.GetConnectionInfo()
+ if err != nil {
+ return nil, err
+ }
+
+ op, err := source.MigrateInstance(instance.Name, sourceReq)
+ if err != nil {
+ return nil, err
+ }
+ opAPI := op.Get()
+
+ sourceSecrets := map[string]string{}
+ for k, v := range opAPI.Metadata {
+ sourceSecrets[k] = v.(string)
+ }
+
+ // Relay mode migration
+ if args != nil && args.Mode == "relay" {
+ // Push copy source fields
+ req.Source.Type = "migration"
+ req.Source.Mode = "push"
+
+ // Start the process
+ targetOp, err := r.CreateInstance(req)
+ if err != nil {
+ return nil, err
+ }
+ targetOpAPI := targetOp.Get()
+
+ // Extract the websockets
+ targetSecrets := map[string]string{}
+ for k, v := range targetOpAPI.Metadata {
+ targetSecrets[k] = v.(string)
+ }
+
+ // Launch the relay
+ err = r.proxyMigration(targetOp.(*operation), targetSecrets, source, op.(*operation), sourceSecrets)
+ if err != nil {
+ return nil, err
+ }
+
+ // Prepare a tracking operation
+ rop := remoteOperation{
+ targetOp: targetOp,
+ chDone: make(chan bool),
+ }
+
+ // Forward targetOp to remote op
+ go func() {
+ rop.err = rop.targetOp.Wait()
+ close(rop.chDone)
+ }()
+
+ return &rop, nil
+ }
+
+ // Pull mode migration
+ req.Source.Type = "migration"
+ req.Source.Mode = "pull"
+ req.Source.Operation = opAPI.ID
+ req.Source.Websockets = sourceSecrets
+ req.Source.Certificate = info.Certificate
+
+ return r.tryCreateInstance(req, info.Addresses)
+}
+
+func (r *ProtocolLXD) proxyInstanceMigration(targetOp *operation, targetSecrets map[string]string, source InstanceServer, sourceOp *operation, sourceSecrets map[string]string) error {
+ // Sanity checks
+ for n := range targetSecrets {
+ _, ok := sourceSecrets[n]
+ if !ok {
+ return fmt.Errorf("Migration target expects the \"%s\" socket but source isn't providing it", n)
+ }
+ }
+
+ if targetSecrets["control"] == "" {
+ return fmt.Errorf("Migration target didn't setup the required \"control\" socket")
+ }
+
+ // Struct used to hold everything together
+ type proxy struct {
+ done chan bool
+ sourceConn *websocket.Conn
+ targetConn *websocket.Conn
+ }
+
+ proxies := map[string]*proxy{}
+
+ // Connect the control socket
+ sourceConn, err := source.GetOperationWebsocket(sourceOp.ID, sourceSecrets["control"])
+ if err != nil {
+ return err
+ }
+
+ targetConn, err := r.GetOperationWebsocket(targetOp.ID, targetSecrets["control"])
+ if err != nil {
+ return err
+ }
+
+ proxies["control"] = &proxy{
+ done: shared.WebsocketProxy(sourceConn, targetConn),
+ sourceConn: sourceConn,
+ targetConn: targetConn,
+ }
+
+ // Connect the data sockets
+ for name := range sourceSecrets {
+ if name == "control" {
+ continue
+ }
+
+ // Handle resets (used for multiple objects)
+ sourceConn, err := source.GetOperationWebsocket(sourceOp.ID, sourceSecrets[name])
+ if err != nil {
+ break
+ }
+
+ targetConn, err := r.GetOperationWebsocket(targetOp.ID, targetSecrets[name])
+ if err != nil {
+ break
+ }
+
+ proxies[name] = &proxy{
+ sourceConn: sourceConn,
+ targetConn: targetConn,
+ done: shared.WebsocketProxy(sourceConn, targetConn),
+ }
+ }
+
+ // Cleanup once everything is done
+ go func() {
+ // Wait for control socket
+ <-proxies["control"].done
+ proxies["control"].sourceConn.Close()
+ proxies["control"].targetConn.Close()
+
+ // Then deal with the others
+ for name, proxy := range proxies {
+ if name == "control" {
+ continue
+ }
+
+ <-proxy.done
+ proxy.sourceConn.Close()
+ proxy.targetConn.Close()
+ }
+ }()
+
+ return nil
+}
+
+// UpdateInstance updates the instance definition.
+func (r *ProtocolLXD) UpdateInstance(name string, instance api.InstancePut, ETag string) (Operation, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("PUT", fmt.Sprintf("%s/%s", path, url.PathEscape(name)), instance, ETag)
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// RenameInstance requests that LXD renames the instance.
+func (r *ProtocolLXD) RenameInstance(name string, instance api.InstancePost) (Operation, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ // Sanity check
+ if instance.Migration {
+ return nil, fmt.Errorf("Can't ask for a migration through RenameInstance")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s", path, url.PathEscape(name)), instance, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+func (r *ProtocolLXD) tryMigrateInstance(source InstanceServer, name string, req api.InstancePost, urls []string) (RemoteOperation, error) {
+ if len(urls) == 0 {
+ return nil, fmt.Errorf("The target server isn't listening on the network")
+ }
+
+ rop := remoteOperation{
+ chDone: make(chan bool),
+ }
+
+ operation := req.Target.Operation
+
+ // Forward targetOp to remote op
+ go func() {
+ success := false
+ errors := map[string]error{}
+ for _, serverURL := range urls {
+ req.Target.Operation = fmt.Sprintf("%s/1.0/operations/%s", serverURL, url.PathEscape(operation))
+
+ op, err := source.MigrateInstance(name, req)
+ if err != nil {
+ errors[serverURL] = err
+ continue
+ }
+
+ rop.targetOp = op
+
+ for _, handler := range rop.handlers {
+ rop.targetOp.AddHandler(handler)
+ }
+
+ err = rop.targetOp.Wait()
+ if err != nil {
+ errors[serverURL] = err
+ continue
+ }
+
+ success = true
+ break
+ }
+
+ if !success {
+ rop.err = remoteOperationError("Failed instance migration", errors)
+ }
+
+ close(rop.chDone)
+ }()
+
+ return &rop, nil
+}
+
+// MigrateInstance requests that LXD prepares for a instance migration.
+func (r *ProtocolLXD) MigrateInstance(name string, instance api.InstancePost) (Operation, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ if instance.ContainerOnly {
+ if !r.HasExtension("container_only_migration") {
+ return nil, fmt.Errorf("The server is missing the required \"container_only_migration\" API extension")
+ }
+ }
+
+ // Sanity check
+ if !instance.Migration {
+ return nil, fmt.Errorf("Can't ask for a rename through MigrateInstance")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s", path, url.PathEscape(name)), instance, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// DeleteInstance requests that LXD deletes the instance.
+func (r *ProtocolLXD) DeleteInstance(name string) (Operation, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("DELETE", fmt.Sprintf("%s/%s", path, url.PathEscape(name)), nil, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// ExecInstance requests that LXD spawns a command inside the instance.
+func (r *ProtocolLXD) ExecInstance(instanceName string, exec api.InstanceExecPost, args *ContainerExecArgs) (Operation, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ if exec.RecordOutput {
+ if !r.HasExtension("container_exec_recording") {
+ return nil, fmt.Errorf("The server is missing the required \"container_exec_recording\" API extension")
+ }
+ }
+
+ if exec.User > 0 || exec.Group > 0 || exec.Cwd != "" {
+ if !r.HasExtension("container_exec_user_group_cwd") {
+ return nil, fmt.Errorf("The server is missing the required \"container_exec_user_group_cwd\" API extension")
+ }
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/exec", path, url.PathEscape(instanceName)), exec, "")
+ if err != nil {
+ return nil, err
+ }
+ opAPI := op.Get()
+
+ // Process additional arguments
+ if args != nil {
+ // Parse the fds
+ fds := map[string]string{}
+
+ value, ok := opAPI.Metadata["fds"]
+ if ok {
+ values := value.(map[string]interface{})
+ for k, v := range values {
+ fds[k] = v.(string)
+ }
+ }
+
+ // Call the control handler with a connection to the control socket
+ if args.Control != nil && fds["control"] != "" {
+ conn, err := r.GetOperationWebsocket(opAPI.ID, fds["control"])
+ if err != nil {
+ return nil, err
+ }
+
+ go args.Control(conn)
+ }
+
+ if exec.Interactive {
+ // Handle interactive sections
+ if args.Stdin != nil && args.Stdout != nil {
+ // Connect to the websocket
+ conn, err := r.GetOperationWebsocket(opAPI.ID, fds["0"])
+ if err != nil {
+ return nil, err
+ }
+
+ // And attach stdin and stdout to it
+ go func() {
+ shared.WebsocketSendStream(conn, args.Stdin, -1)
+ <-shared.WebsocketRecvStream(args.Stdout, conn)
+ conn.Close()
+
+ if args.DataDone != nil {
+ close(args.DataDone)
+ }
+ }()
+ } else {
+ if args.DataDone != nil {
+ close(args.DataDone)
+ }
+ }
+ } else {
+ // Handle non-interactive sessions
+ dones := map[int]chan bool{}
+ conns := []*websocket.Conn{}
+
+ // Handle stdin
+ if fds["0"] != "" {
+ conn, err := r.GetOperationWebsocket(opAPI.ID, fds["0"])
+ if err != nil {
+ return nil, err
+ }
+
+ conns = append(conns, conn)
+ dones[0] = shared.WebsocketSendStream(conn, args.Stdin, -1)
+ }
+
+ // Handle stdout
+ if fds["1"] != "" {
+ conn, err := r.GetOperationWebsocket(opAPI.ID, fds["1"])
+ if err != nil {
+ return nil, err
+ }
+
+ conns = append(conns, conn)
+ dones[1] = shared.WebsocketRecvStream(args.Stdout, conn)
+ }
+
+ // Handle stderr
+ if fds["2"] != "" {
+ conn, err := r.GetOperationWebsocket(opAPI.ID, fds["2"])
+ if err != nil {
+ return nil, err
+ }
+
+ conns = append(conns, conn)
+ dones[2] = shared.WebsocketRecvStream(args.Stderr, conn)
+ }
+
+ // Wait for everything to be done
+ go func() {
+ for i, chDone := range dones {
+ // Skip stdin, dealing with it separately below
+ if i == 0 {
+ continue
+ }
+
+ <-chDone
+ }
+
+ if fds["0"] != "" {
+ if args.Stdin != nil {
+ args.Stdin.Close()
+ }
+
+ // Empty the stdin channel but don't block on it as
+ // stdin may be stuck in Read()
+ go func() {
+ <-dones[0]
+ }()
+ }
+
+ for _, conn := range conns {
+ conn.Close()
+ }
+
+ if args.DataDone != nil {
+ close(args.DataDone)
+ }
+ }()
+ }
+ }
+
+ return op, nil
+}
+
+// GetInstanceFile retrieves the provided path from the instance.
+func (r *ProtocolLXD) GetInstanceFile(instanceName string, path string) (io.ReadCloser, *ContainerFileResponse, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // Prepare the HTTP request
+ requestURL, err := shared.URLEncode(
+ fmt.Sprintf("%s/1.0%s/%s/files", r.httpHost, path, url.PathEscape(instanceName)),
+ map[string]string{"path": path})
+ if err != nil {
+ return nil, nil, err
+ }
+
+ requestURL, err = r.setQueryAttributes(requestURL)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ req, err := http.NewRequest("GET", requestURL, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // Set the user agent
+ if r.httpUserAgent != "" {
+ req.Header.Set("User-Agent", r.httpUserAgent)
+ }
+
+ // Send the request
+ resp, err := r.do(req)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // Check the return value for a cleaner error
+ if resp.StatusCode != http.StatusOK {
+ _, _, err := lxdParseResponse(resp)
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+
+ // Parse the headers
+ uid, gid, mode, fileType, _ := shared.ParseLXDFileHeaders(resp.Header)
+ fileResp := ContainerFileResponse{
+ UID: uid,
+ GID: gid,
+ Mode: mode,
+ Type: fileType,
+ }
+
+ if fileResp.Type == "directory" {
+ // Decode the response
+ response := api.Response{}
+ decoder := json.NewDecoder(resp.Body)
+
+ err = decoder.Decode(&response)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // Get the file list
+ entries := []string{}
+ err = response.MetadataAsStruct(&entries)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ fileResp.Entries = entries
+
+ return nil, &fileResp, err
+ }
+
+ return resp.Body, &fileResp, err
+}
+
+// CreateInstanceFile tells LXD to create a file in the instance.
+func (r *ProtocolLXD) CreateInstanceFile(instanceName string, path string, args ContainerFileArgs) error {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return err
+ }
+
+ if args.Type == "directory" {
+ if !r.HasExtension("directory_manipulation") {
+ return fmt.Errorf("The server is missing the required \"directory_manipulation\" API extension")
+ }
+ }
+
+ if args.Type == "symlink" {
+ if !r.HasExtension("file_symlinks") {
+ return fmt.Errorf("The server is missing the required \"file_symlinks\" API extension")
+ }
+ }
+
+ if args.WriteMode == "append" {
+ if !r.HasExtension("file_append") {
+ return fmt.Errorf("The server is missing the required \"file_append\" API extension")
+ }
+ }
+
+ // Prepare the HTTP request
+ requestURL := fmt.Sprintf("%s/1.0%s/%s/files?path=%s", r.httpHost, path, url.PathEscape(instanceName), url.QueryEscape(path))
+
+ requestURL, err = r.setQueryAttributes(requestURL)
+ if err != nil {
+ return err
+ }
+
+ req, err := http.NewRequest("POST", requestURL, args.Content)
+ if err != nil {
+ return err
+ }
+
+ // Set the user agent
+ if r.httpUserAgent != "" {
+ req.Header.Set("User-Agent", r.httpUserAgent)
+ }
+
+ // Set the various headers
+ if args.UID > -1 {
+ req.Header.Set("X-LXD-uid", fmt.Sprintf("%d", args.UID))
+ }
+
+ if args.GID > -1 {
+ req.Header.Set("X-LXD-gid", fmt.Sprintf("%d", args.GID))
+ }
+
+ if args.Mode > -1 {
+ req.Header.Set("X-LXD-mode", fmt.Sprintf("%04o", args.Mode))
+ }
+
+ if args.Type != "" {
+ req.Header.Set("X-LXD-type", args.Type)
+ }
+
+ if args.WriteMode != "" {
+ req.Header.Set("X-LXD-write", args.WriteMode)
+ }
+
+ // Send the request
+ resp, err := r.do(req)
+ if err != nil {
+ return err
+ }
+
+ // Check the return value for a cleaner error
+ _, _, err = lxdParseResponse(resp)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// DeleteInstanceFile deletes a file in the instance.
+func (r *ProtocolLXD) DeleteInstanceFile(instanceName string, path string) error {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return err
+ }
+
+ if !r.HasExtension("file_delete") {
+ return fmt.Errorf("The server is missing the required \"file_delete\" API extension")
+ }
+
+ // Send the request
+ _, _, err = r.query("DELETE", fmt.Sprintf("%s/%s/files?path=%s", path, url.PathEscape(instanceName), url.QueryEscape(path)), nil, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// GetInstanceSnapshotNames returns a list of snapshot names for the instance.
+func (r *ProtocolLXD) GetInstanceSnapshotNames(instanceName string) ([]string, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ urls := []string{}
+
+ // Fetch the raw value
+ _, err = r.queryStruct("GET", fmt.Sprintf("%s/%s/snapshots", path, url.PathEscape(instanceName)), nil, "", &urls)
+ if err != nil {
+ return nil, err
+ }
+
+ // Parse it
+ names := []string{}
+ for _, uri := range urls {
+ fields := strings.Split(uri, fmt.Sprintf("%s/%s/snapshots/", path, url.PathEscape(instanceName)))
+ names = append(names, fields[len(fields)-1])
+ }
+
+ return names, nil
+}
+
+// GetInstanceSnapshots returns a list of snapshots for the instance.
+func (r *ProtocolLXD) GetInstanceSnapshots(instanceName string) ([]api.InstanceSnapshot, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ snapshots := []api.InstanceSnapshot{}
+
+ // Fetch the raw value
+ _, err = r.queryStruct("GET", fmt.Sprintf("%s/%s/snapshots?recursion=1", path, url.PathEscape(instanceName)), nil, "", &snapshots)
+ if err != nil {
+ return nil, err
+ }
+
+ return snapshots, nil
+}
+
+// GetInstanceSnapshot returns a Snapshot struct for the provided instance and snapshot names
+func (r *ProtocolLXD) GetInstanceSnapshot(instanceName string, name string) (*api.InstanceSnapshot, string, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, "", err
+ }
+
+ snapshot := api.InstanceSnapshot{}
+
+ // Fetch the raw value
+ etag, err := r.queryStruct("GET", fmt.Sprintf("%s/%s/snapshots/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), nil, "", &snapshot)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return &snapshot, etag, nil
+}
+
+// CreateInstanceSnapshot requests that LXD creates a new snapshot for the instance.
+func (r *ProtocolLXD) CreateInstanceSnapshot(instanceName string, snapshot api.InstanceSnapshotsPost) (Operation, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ // Validate the request
+ if snapshot.ExpiresAt != nil && !r.HasExtension("snapshot_expiry_creation") {
+ return nil, fmt.Errorf("The server is missing the required \"snapshot_expiry_creation\" API extension")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/snapshots", path, url.PathEscape(instanceName)), snapshot, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// CopyInstanceSnapshot copies a snapshot from a remote server into a new instance. Additional options can be passed using InstanceCopyArgs.
+func (r *ProtocolLXD) CopyInstanceSnapshot(source InstanceServer, instanceName string, snapshot api.InstanceSnapshot, args *ContainerSnapshotCopyArgs) (RemoteOperation, error) {
+ // Backward compatibility (with broken Name field)
+ fields := strings.Split(snapshot.Name, shared.SnapshotDelimiter)
+ cName := instanceName
+ sName := fields[len(fields)-1]
+
+ // Base request
+ req := api.InstancesPost{
+ Name: cName,
+ InstancePut: api.InstancePut{
+ Architecture: snapshot.Architecture,
+ Config: snapshot.Config,
+ Devices: snapshot.Devices,
+ Ephemeral: snapshot.Ephemeral,
+ Profiles: snapshot.Profiles,
+ },
+ }
+
+ if snapshot.Stateful && args.Live {
+ if !r.HasExtension("container_snapshot_stateful_migration") {
+ return nil, fmt.Errorf("The server is missing the required \"container_snapshot_stateful_migration\" API extension")
+ }
+ req.InstancePut.Stateful = snapshot.Stateful
+ req.Source.Live = args.Live
+ }
+ req.Source.BaseImage = snapshot.Config["volatile.base_image"]
+
+ // Process the copy arguments
+ if args != nil {
+ // Sanity checks
+ if shared.StringInSlice(args.Mode, []string{"push", "relay"}) {
+ if !r.HasExtension("container_push") {
+ return nil, fmt.Errorf("The target server is missing the required \"container_push\" API extension")
+ }
+
+ if !source.HasExtension("container_push") {
+ return nil, fmt.Errorf("The source server is missing the required \"container_push\" API extension")
+ }
+ }
+
+ if args.Mode == "push" && !source.HasExtension("container_push_target") {
+ return nil, fmt.Errorf("The source server is missing the required \"container_push_target\" API extension")
+ }
+
+ // Allow overriding the target name
+ if args.Name != "" {
+ req.Name = args.Name
+ }
+ }
+
+ sourceInfo, err := source.GetConnectionInfo()
+ if err != nil {
+ return nil, fmt.Errorf("Failed to get source connection info: %v", err)
+ }
+
+ destInfo, err := r.GetConnectionInfo()
+ if err != nil {
+ return nil, fmt.Errorf("Failed to get destination connection info: %v", err)
+ }
+
+ instance, _, err := source.GetInstance(cName)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to get instance info: %v", err)
+ }
+
+ // Optimization for the local copy case
+ if destInfo.URL == sourceInfo.URL && destInfo.SocketPath == sourceInfo.SocketPath && (!r.IsClustered() || instance.Location == r.clusterTarget || r.HasExtension("cluster_internal_copy")) {
+ // Project handling
+ if destInfo.Project != sourceInfo.Project {
+ if !r.HasExtension("container_copy_project") {
+ return nil, fmt.Errorf("The server is missing the required \"container_copy_project\" API extension")
+ }
+
+ req.Source.Project = sourceInfo.Project
+ }
+
+ // Local copy source fields
+ req.Source.Type = "copy"
+ req.Source.Source = fmt.Sprintf("%s/%s", cName, sName)
+
+ // Copy the instance
+ op, err := r.CreateInstance(req)
+ if err != nil {
+ return nil, err
+ }
+
+ rop := remoteOperation{
+ targetOp: op,
+ chDone: make(chan bool),
+ }
+
+ // Forward targetOp to remote op
+ go func() {
+ rop.err = rop.targetOp.Wait()
+ close(rop.chDone)
+ }()
+
+ return &rop, nil
+ }
+
+ // Source request
+ sourceReq := api.InstanceSnapshotPost{
+ Migration: true,
+ Name: args.Name,
+ }
+ if snapshot.Stateful && args.Live {
+ sourceReq.Live = args.Live
+ }
+
+ // Push mode migration
+ if args != nil && args.Mode == "push" {
+ // Get target server connection information
+ info, err := r.GetConnectionInfo()
+ if err != nil {
+ return nil, err
+ }
+
+ // Create the instance
+ req.Source.Type = "migration"
+ req.Source.Mode = "push"
+
+ op, err := r.CreateInstance(req)
+ if err != nil {
+ return nil, err
+ }
+ opAPI := op.Get()
+
+ targetSecrets := map[string]string{}
+ for k, v := range opAPI.Metadata {
+ targetSecrets[k] = v.(string)
+ }
+
+ // Prepare the source request
+ target := api.InstancePostTarget{}
+ target.Operation = opAPI.ID
+ target.Websockets = targetSecrets
+ target.Certificate = info.Certificate
+ sourceReq.Target = &target
+
+ return r.tryMigrateInstanceSnapshot(source, cName, sName, sourceReq, info.Addresses)
+ }
+
+ // Get source server connection information
+ info, err := source.GetConnectionInfo()
+ if err != nil {
+ return nil, err
+ }
+
+ op, err := source.MigrateInstanceSnapshot(cName, sName, sourceReq)
+ if err != nil {
+ return nil, err
+ }
+ opAPI := op.Get()
+
+ sourceSecrets := map[string]string{}
+ for k, v := range opAPI.Metadata {
+ sourceSecrets[k] = v.(string)
+ }
+
+ // Relay mode migration
+ if args != nil && args.Mode == "relay" {
+ // Push copy source fields
+ req.Source.Type = "migration"
+ req.Source.Mode = "push"
+
+ // Start the process
+ targetOp, err := r.CreateInstance(req)
+ if err != nil {
+ return nil, err
+ }
+ targetOpAPI := targetOp.Get()
+
+ // Extract the websockets
+ targetSecrets := map[string]string{}
+ for k, v := range targetOpAPI.Metadata {
+ targetSecrets[k] = v.(string)
+ }
+
+ // Launch the relay
+ err = r.proxyMigration(targetOp.(*operation), targetSecrets, source, op.(*operation), sourceSecrets)
+ if err != nil {
+ return nil, err
+ }
+
+ // Prepare a tracking operation
+ rop := remoteOperation{
+ targetOp: targetOp,
+ chDone: make(chan bool),
+ }
+
+ // Forward targetOp to remote op
+ go func() {
+ rop.err = rop.targetOp.Wait()
+ close(rop.chDone)
+ }()
+
+ return &rop, nil
+ }
+
+ // Pull mode migration
+ req.Source.Type = "migration"
+ req.Source.Mode = "pull"
+ req.Source.Operation = opAPI.ID
+ req.Source.Websockets = sourceSecrets
+ req.Source.Certificate = info.Certificate
+
+ return r.tryCreateInstance(req, info.Addresses)
+}
+
+// RenameInstanceSnapshot requests that LXD renames the snapshot.
+func (r *ProtocolLXD) RenameInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPost) (Operation, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ // Sanity check
+ if instance.Migration {
+ return nil, fmt.Errorf("Can't ask for a migration through RenameInstanceSnapshot")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/snapshots/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), instance, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+func (r *ProtocolLXD) tryMigrateInstanceSnapshot(source InstanceServer, instanceName string, name string, req api.InstanceSnapshotPost, urls []string) (RemoteOperation, error) {
+ if len(urls) == 0 {
+ return nil, fmt.Errorf("The target server isn't listening on the network")
+ }
+
+ rop := remoteOperation{
+ chDone: make(chan bool),
+ }
+
+ operation := req.Target.Operation
+
+ // Forward targetOp to remote op
+ go func() {
+ success := false
+ errors := map[string]error{}
+ for _, serverURL := range urls {
+ req.Target.Operation = fmt.Sprintf("%s/1.0/operations/%s", serverURL, url.PathEscape(operation))
+
+ op, err := source.MigrateInstanceSnapshot(instanceName, name, req)
+ if err != nil {
+ errors[serverURL] = err
+ continue
+ }
+
+ rop.targetOp = op
+
+ for _, handler := range rop.handlers {
+ rop.targetOp.AddHandler(handler)
+ }
+
+ err = rop.targetOp.Wait()
+ if err != nil {
+ errors[serverURL] = err
+ continue
+ }
+
+ success = true
+ break
+ }
+
+ if !success {
+ rop.err = remoteOperationError("Failed instance migration", errors)
+ }
+
+ close(rop.chDone)
+ }()
+
+ return &rop, nil
+}
+
+// MigrateInstanceSnapshot requests that LXD prepares for a snapshot migration.
+func (r *ProtocolLXD) MigrateInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPost) (Operation, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ // Sanity check
+ if !instance.Migration {
+ return nil, fmt.Errorf("Can't ask for a rename through MigrateInstanceSnapshot")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/snapshots/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), instance, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// DeleteInstanceSnapshot requests that LXD deletes the instance snapshot.
+func (r *ProtocolLXD) DeleteInstanceSnapshot(instanceName string, name string) (Operation, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("DELETE", fmt.Sprintf("%s/%s/snapshots/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), nil, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// UpdateInstanceSnapshot requests that LXD updates the instance snapshot.
+func (r *ProtocolLXD) UpdateInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPut, ETag string) (Operation, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ if !r.HasExtension("snapshot_expiry") {
+ return nil, fmt.Errorf("The server is missing the required \"snapshot_expiry\" API extension")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("PUT", fmt.Sprintf("%s/%s/snapshots/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), instance, ETag)
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// GetInstanceState returns a InstanceState entry for the provided instance name.
+func (r *ProtocolLXD) GetInstanceState(name string) (*api.InstanceState, string, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, "", err
+ }
+
+ state := api.InstanceState{}
+
+ // Fetch the raw value
+ etag, err := r.queryStruct("GET", fmt.Sprintf("%s/%s/state", path, url.PathEscape(name)), nil, "", &state)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return &state, etag, nil
+}
+
+// UpdateInstanceState updates the instance to match the requested state.
+func (r *ProtocolLXD) UpdateInstanceState(name string, state api.InstanceStatePut, ETag string) (Operation, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("PUT", fmt.Sprintf("%s/%s/state", path, url.PathEscape(name)), state, ETag)
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// GetInstanceLogfiles returns a list of logfiles for the instance.
+func (r *ProtocolLXD) GetInstanceLogfiles(name string) ([]string, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ urls := []string{}
+
+ // Fetch the raw value
+ _, err = r.queryStruct("GET", fmt.Sprintf("%s/%s/logs", path, url.PathEscape(name)), nil, "", &urls)
+ if err != nil {
+ return nil, err
+ }
+
+ // Parse it
+ logfiles := []string{}
+ for _, uri := range logfiles {
+ fields := strings.Split(uri, fmt.Sprintf("%s/%s/logs/", path, url.PathEscape(name)))
+ logfiles = append(logfiles, fields[len(fields)-1])
+ }
+
+ return logfiles, nil
+}
+
+// GetInstanceLogfile returns the content of the requested logfile.
+//
+// Note that it's the caller's responsibility to close the returned ReadCloser
+func (r *ProtocolLXD) GetInstanceLogfile(name string, filename string) (io.ReadCloser, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ // Prepare the HTTP request
+ url := fmt.Sprintf("%s/1.0%s/%s/logs/%s", r.httpHost, path, url.PathEscape(name), url.PathEscape(filename))
+
+ url, err = r.setQueryAttributes(url)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ // Set the user agent
+ if r.httpUserAgent != "" {
+ req.Header.Set("User-Agent", r.httpUserAgent)
+ }
+
+ // Send the request
+ resp, err := r.do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ // Check the return value for a cleaner error
+ if resp.StatusCode != http.StatusOK {
+ _, _, err := lxdParseResponse(resp)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return resp.Body, err
+}
+
+// DeleteInstanceLogfile deletes the requested logfile.
+func (r *ProtocolLXD) DeleteInstanceLogfile(name string, filename string) error {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return err
+ }
+
+ // Send the request
+ _, _, err = r.query("DELETE", fmt.Sprintf("%s/%s/logs/%s", path, url.PathEscape(name), url.PathEscape(filename)), nil, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// GetInstanceMetadata returns instance metadata.
+func (r *ProtocolLXD) GetInstanceMetadata(name string) (*api.ImageMetadata, string, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, "", err
+ }
+
+ if !r.HasExtension("container_edit_metadata") {
+ return nil, "", fmt.Errorf("The server is missing the required \"container_edit_metadata\" API extension")
+ }
+
+ metadata := api.ImageMetadata{}
+
+ url := fmt.Sprintf("%s/%s/metadata", path, url.PathEscape(name))
+ etag, err := r.queryStruct("GET", url, nil, "", &metadata)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return &metadata, etag, err
+}
+
+// SetInstanceMetadata sets the content of the instance metadata file.
+func (r *ProtocolLXD) SetInstanceMetadata(name string, metadata api.ImageMetadata, ETag string) error {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return err
+ }
+
+ if !r.HasExtension("container_edit_metadata") {
+ return fmt.Errorf("The server is missing the required \"container_edit_metadata\" API extension")
+ }
+
+ url := fmt.Sprintf("%s/%s/metadata", path, url.PathEscape(name))
+ _, _, err = r.query("PUT", url, metadata, ETag)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// GetInstanceTemplateFiles returns the list of names of template files for a instance.
+func (r *ProtocolLXD) GetInstanceTemplateFiles(instanceName string) ([]string, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ if !r.HasExtension("container_edit_metadata") {
+ return nil, fmt.Errorf("The server is missing the required \"container_edit_metadata\" API extension")
+ }
+
+ templates := []string{}
+
+ url := fmt.Sprintf("%s/%s/metadata/templates", path, url.PathEscape(instanceName))
+ _, err = r.queryStruct("GET", url, nil, "", &templates)
+ if err != nil {
+ return nil, err
+ }
+
+ return templates, nil
+}
+
+// GetInstanceTemplateFile returns the content of a template file for a instance.
+func (r *ProtocolLXD) GetInstanceTemplateFile(instanceName string, templateName string) (io.ReadCloser, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ if !r.HasExtension("container_edit_metadata") {
+ return nil, fmt.Errorf("The server is missing the required \"container_edit_metadata\" API extension")
+ }
+
+ url := fmt.Sprintf("%s/1.0%s/%s/metadata/templates?path=%s", r.httpHost, path, url.PathEscape(instanceName), url.QueryEscape(templateName))
+
+ url, err = r.setQueryAttributes(url)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ // Set the user agent
+ if r.httpUserAgent != "" {
+ req.Header.Set("User-Agent", r.httpUserAgent)
+ }
+
+ // Send the request
+ resp, err := r.do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ // Check the return value for a cleaner error
+ if resp.StatusCode != http.StatusOK {
+ _, _, err := lxdParseResponse(resp)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return resp.Body, err
+}
+
+// CreateInstanceTemplateFile creates an a template for a instance.
+func (r *ProtocolLXD) CreateInstanceTemplateFile(instanceName string, templateName string, content io.ReadSeeker) error {
+ return r.setInstanceTemplateFile(instanceName, templateName, content, "POST")
+}
+
+// UpdateInstanceTemplateFile updates the content for a instance template file.
+func (r *ProtocolLXD) UpdateInstanceTemplateFile(instanceName string, templateName string, content io.ReadSeeker) error {
+ return r.setInstanceTemplateFile(instanceName, templateName, content, "PUT")
+}
+
+func (r *ProtocolLXD) setInstanceTemplateFile(instanceName string, templateName string, content io.ReadSeeker, httpMethod string) error {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return err
+ }
+
+ if !r.HasExtension("container_edit_metadata") {
+ return fmt.Errorf("The server is missing the required \"container_edit_metadata\" API extension")
+ }
+
+ url := fmt.Sprintf("%s/1.0%s/%s/metadata/templates?path=%s", r.httpHost, path, url.PathEscape(instanceName), url.QueryEscape(templateName))
+
+ url, err = r.setQueryAttributes(url)
+ if err != nil {
+ return err
+ }
+
+ req, err := http.NewRequest(httpMethod, url, content)
+ if err != nil {
+ return err
+ }
+ req.Header.Set("Content-Type", "application/octet-stream")
+
+ // Set the user agent
+ if r.httpUserAgent != "" {
+ req.Header.Set("User-Agent", r.httpUserAgent)
+ }
+
+ // Send the request
+ resp, err := r.do(req)
+ // Check the return value for a cleaner error
+ if resp.StatusCode != http.StatusOK {
+ _, _, err := lxdParseResponse(resp)
+ if err != nil {
+ return err
+ }
+ }
+ return err
+}
+
+// DeleteInstanceTemplateFile deletes a template file for a instance.
+func (r *ProtocolLXD) DeleteInstanceTemplateFile(name string, templateName string) error {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return err
+ }
+
+ if !r.HasExtension("container_edit_metadata") {
+ return fmt.Errorf("The server is missing the required \"container_edit_metadata\" API extension")
+ }
+ _, _, err = r.query("DELETE", fmt.Sprintf("%s/%s/metadata/templates?path=%s", path, url.PathEscape(name), url.QueryEscape(templateName)), nil, "")
+ return err
+}
+
+// ConsoleInstance requests that LXD attaches to the console device of a instance.
+func (r *ProtocolLXD) ConsoleInstance(instanceName string, console api.InstanceConsolePost, args *ContainerConsoleArgs) (Operation, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ if !r.HasExtension("console") {
+ return nil, fmt.Errorf("The server is missing the required \"console\" API extension")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/console", path, url.PathEscape(instanceName)), console, "")
+ if err != nil {
+ return nil, err
+ }
+ opAPI := op.Get()
+
+ if args == nil || args.Terminal == nil {
+ return nil, fmt.Errorf("A terminal must be set")
+ }
+
+ if args.Control == nil {
+ return nil, fmt.Errorf("A control channel must be set")
+ }
+
+ // Parse the fds
+ fds := map[string]string{}
+
+ value, ok := opAPI.Metadata["fds"]
+ if ok {
+ values := value.(map[string]interface{})
+ for k, v := range values {
+ fds[k] = v.(string)
+ }
+ }
+
+ var controlConn *websocket.Conn
+ // Call the control handler with a connection to the control socket
+ if fds["control"] == "" {
+ return nil, fmt.Errorf("Did not receive a file descriptor for the control channel")
+ }
+
+ controlConn, err = r.GetOperationWebsocket(opAPI.ID, fds["control"])
+ if err != nil {
+ return nil, err
+ }
+
+ go args.Control(controlConn)
+
+ // Connect to the websocket
+ conn, err := r.GetOperationWebsocket(opAPI.ID, fds["0"])
+ if err != nil {
+ return nil, err
+ }
+
+ // Detach from console.
+ go func(consoleDisconnect <-chan bool) {
+ <-consoleDisconnect
+ msg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "Detaching from console")
+ // We don't care if this fails. This is just for convenience.
+ controlConn.WriteMessage(websocket.CloseMessage, msg)
+ controlConn.Close()
+ }(args.ConsoleDisconnect)
+
+ // And attach stdin and stdout to it
+ go func() {
+ shared.WebsocketSendStream(conn, args.Terminal, -1)
+ <-shared.WebsocketRecvStream(args.Terminal, conn)
+ conn.Close()
+ }()
+
+ return op, nil
+}
+
+// GetInstanceConsoleLog requests that LXD attaches to the console device of a instance.
+//
+// Note that it's the caller's responsibility to close the returned ReadCloser
+func (r *ProtocolLXD) GetInstanceConsoleLog(instanceName string, args *ContainerConsoleLogArgs) (io.ReadCloser, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ if !r.HasExtension("console") {
+ return nil, fmt.Errorf("The server is missing the required \"console\" API extension")
+ }
+
+ // Prepare the HTTP request
+ url := fmt.Sprintf("%s/1.0%s/%s/console", r.httpHost, path, url.PathEscape(instanceName))
+
+ url, err = r.setQueryAttributes(url)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ // Set the user agent
+ if r.httpUserAgent != "" {
+ req.Header.Set("User-Agent", r.httpUserAgent)
+ }
+
+ // Send the request
+ resp, err := r.do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ // Check the return value for a cleaner error
+ if resp.StatusCode != http.StatusOK {
+ _, _, err := lxdParseResponse(resp)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return resp.Body, err
+}
+
+// DeleteInstanceConsoleLog deletes the requested instance's console log.
+func (r *ProtocolLXD) DeleteInstanceConsoleLog(instanceName string, args *ContainerConsoleLogArgs) error {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return err
+ }
+
+ if !r.HasExtension("console") {
+ return fmt.Errorf("The server is missing the required \"console\" API extension")
+ }
+
+ // Send the request
+ _, _, err = r.query("DELETE", fmt.Sprintf("%s/%s/console", path, url.PathEscape(instanceName)), nil, "")
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// GetInstanceBackupNames returns a list of backup names for the instance.
+func (r *ProtocolLXD) GetInstanceBackupNames(instanceName string) ([]string, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ if !r.HasExtension("container_backup") {
+ return nil, fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+ }
+
+ // Fetch the raw value
+ urls := []string{}
+ _, err = r.queryStruct("GET", fmt.Sprintf("%s/%s/backups", path, url.PathEscape(instanceName)), nil, "", &urls)
+ if err != nil {
+ return nil, err
+ }
+
+ // Parse it
+ names := []string{}
+ for _, uri := range urls {
+ fields := strings.Split(uri, fmt.Sprintf("%s/%s/backups/", path, url.PathEscape(instanceName)))
+ names = append(names, fields[len(fields)-1])
+ }
+
+ return names, nil
+}
+
+// GetInstanceBackups returns a list of backups for the instance.
+func (r *ProtocolLXD) GetInstanceBackups(instanceName string) ([]api.InstanceBackup, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ if !r.HasExtension("container_backup") {
+ return nil, fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+ }
+
+ // Fetch the raw value
+ backups := []api.InstanceBackup{}
+
+ _, err = r.queryStruct("GET", fmt.Sprintf("%s/%s/backups?recursion=1", path, url.PathEscape(instanceName)), nil, "", &backups)
+ if err != nil {
+ return nil, err
+ }
+
+ return backups, nil
+}
+
+// GetInstanceBackup returns a Backup struct for the provided instance and backup names.
+func (r *ProtocolLXD) GetInstanceBackup(instanceName string, name string) (*api.InstanceBackup, string, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, "", err
+ }
+
+ if !r.HasExtension("container_backup") {
+ return nil, "", fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+ }
+
+ // Fetch the raw value
+ backup := api.InstanceBackup{}
+ etag, err := r.queryStruct("GET", fmt.Sprintf("%s/%s/backups/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), nil, "", &backup)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return &backup, etag, nil
+}
+
+// CreateInstanceBackup requests that LXD creates a new backup for the instance.
+func (r *ProtocolLXD) CreateInstanceBackup(instanceName string, backup api.InstanceBackupsPost) (Operation, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ if !r.HasExtension("container_backup") {
+ return nil, fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/backups", path, url.PathEscape(instanceName)), backup, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// RenameInstanceBackup requests that LXD renames the backup.
+func (r *ProtocolLXD) RenameInstanceBackup(instanceName string, name string, backup api.InstanceBackupPost) (Operation, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ if !r.HasExtension("container_backup") {
+ return nil, fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/backups/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), backup, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// DeleteInstanceBackup requests that LXD deletes the instance backup.
+func (r *ProtocolLXD) DeleteInstanceBackup(instanceName string, name string) (Operation, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ if !r.HasExtension("container_backup") {
+ return nil, fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+ }
+
+ // Send the request
+ op, _, err := r.queryOperation("DELETE", fmt.Sprintf("%s/%s/backups/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), nil, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return op, nil
+}
+
+// GetInstanceBackupFile requests the instance backup content.
+func (r *ProtocolLXD) GetInstanceBackupFile(instanceName string, name string, req *BackupFileRequest) (*BackupFileResponse, error) {
+ path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
+ if err != nil {
+ return nil, err
+ }
+
+ if !r.HasExtension("container_backup") {
+ return nil, fmt.Errorf("The server is missing the required \"container_backup\" API extension")
+ }
+
+ // Build the URL
+ uri := fmt.Sprintf("%s/1.0%s/%s/backups/%s/export", r.httpHost, path, url.PathEscape(instanceName), url.PathEscape(name))
+ if r.project != "" {
+ uri += fmt.Sprintf("?project=%s", url.QueryEscape(r.project))
+ }
+
+ // Prepare the download request
+ request, err := http.NewRequest("GET", uri, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ if r.httpUserAgent != "" {
+ request.Header.Set("User-Agent", r.httpUserAgent)
+ }
+
+ // Start the request
+ response, doneCh, err := cancel.CancelableDownload(req.Canceler, r.http, request)
+ if err != nil {
+ return nil, err
+ }
+ defer response.Body.Close()
+ defer close(doneCh)
+
+ if response.StatusCode != http.StatusOK {
+ _, _, err := lxdParseResponse(response)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // Handle the data
+ body := response.Body
+ if req.ProgressHandler != nil {
+ body = &ioprogress.ProgressReader{
+ ReadCloser: response.Body,
+ Tracker: &ioprogress.ProgressTracker{
+ Length: response.ContentLength,
+ Handler: func(percent int64, speed int64) {
+ req.ProgressHandler(ioprogress.ProgressData{Text: fmt.Sprintf("%d%% (%s/s)", percent, units.GetByteSizeString(speed, 2))})
+ },
+ },
+ }
+ }
+
+ size, err := io.Copy(req.BackupFile, body)
+ if err != nil {
+ return nil, err
+ }
+
+ resp := BackupFileResponse{}
+ resp.Size = size
+
+ return &resp, nil
+}
From 6b9769a8c026549a45f32228efe5fa69c3e6f262 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Thu, 12 Sep 2019 11:59:51 +0100
Subject: [PATCH 3/3] lxc: Switches cli tool to use InstanceServer
Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
lxc/action.go | 4 ++--
lxc/config/remote.go | 4 ++--
lxc/console.go | 2 +-
lxc/copy.go | 6 +++---
lxc/delete.go | 2 +-
lxc/exec.go | 2 +-
lxc/export.go | 2 +-
lxc/file.go | 8 ++++----
lxc/image.go | 2 +-
lxc/info.go | 6 +++---
lxc/init.go | 8 ++++----
lxc/list.go | 4 ++--
lxc/main.go | 6 +++---
lxc/monitor.go | 2 +-
lxc/move.go | 4 ++--
lxc/project.go | 2 +-
lxc/publish.go | 4 ++--
lxc/query.go | 2 +-
lxc/remote.go | 20 ++++++++++----------
lxc/restore.go | 2 +-
lxc/snapshot.go | 2 +-
lxc/utils.go | 6 +++---
22 files changed, 50 insertions(+), 50 deletions(-)
diff --git a/lxc/action.go b/lxc/action.go
index e41e5f530e..a1ac453bad 100644
--- a/lxc/action.go
+++ b/lxc/action.go
@@ -142,7 +142,7 @@ func (c *cmdAction) doAction(action string, conf *config.Config, nameArg string)
return err
}
- d, err := conf.GetContainerServer(remote)
+ d, err := conf.GetInstanceServer(remote)
if err != nil {
return err
}
@@ -211,7 +211,7 @@ func (c *cmdAction) Run(cmd *cobra.Command, args []string) error {
return nil
}
- d, err := conf.GetContainerServer(conf.DefaultRemote)
+ d, err := conf.GetInstanceServer(conf.DefaultRemote)
if err != nil {
return err
}
diff --git a/lxc/config/remote.go b/lxc/config/remote.go
index cd7a7d8eca..40f64a9d9c 100644
--- a/lxc/config/remote.go
+++ b/lxc/config/remote.go
@@ -50,8 +50,8 @@ func (c *Config) ParseRemote(raw string) (string, string, error) {
return result[0], result[1], nil
}
-// GetContainerServer returns a ContainerServer struct for the remote
-func (c *Config) GetContainerServer(name string) (lxd.ContainerServer, error) {
+// GetInstanceServer returns a InstanceServer struct for the remote
+func (c *Config) GetInstanceServer(name string) (lxd.InstanceServer, error) {
// Handle "local" on non-Linux
if name == "local" && runtime.GOOS != "linux" {
return nil, ErrNotLinux
diff --git a/lxc/console.go b/lxc/console.go
index 9404a0f89f..2b096e5074 100644
--- a/lxc/console.go
+++ b/lxc/console.go
@@ -120,7 +120,7 @@ func (c *cmdConsole) Run(cmd *cobra.Command, args []string) error {
return err
}
- d, err := conf.GetContainerServer(remote)
+ d, err := conf.GetInstanceServer(remote)
if err != nil {
return err
}
diff --git a/lxc/copy.go b/lxc/copy.go
index ae3c2ddba9..bd9d515888 100644
--- a/lxc/copy.go
+++ b/lxc/copy.go
@@ -88,19 +88,19 @@ func (c *cmdCopy) copyContainer(conf *config.Config, sourceResource string,
}
// Connect to the source host
- source, err := conf.GetContainerServer(sourceRemote)
+ source, err := conf.GetInstanceServer(sourceRemote)
if err != nil {
return err
}
// Connect to the destination host
- var dest lxd.ContainerServer
+ var dest lxd.InstanceServer
if sourceRemote == destRemote {
// Source and destination are the same
dest = source
} else {
// Destination is different, connect to it
- dest, err = conf.GetContainerServer(destRemote)
+ dest, err = conf.GetInstanceServer(destRemote)
if err != nil {
return err
}
diff --git a/lxc/delete.go b/lxc/delete.go
index 8b89fc05d0..c6103c6b15 100644
--- a/lxc/delete.go
+++ b/lxc/delete.go
@@ -50,7 +50,7 @@ func (c *cmdDelete) promptDelete(name string) error {
return nil
}
-func (c *cmdDelete) doDelete(d lxd.ContainerServer, name string) error {
+func (c *cmdDelete) doDelete(d lxd.InstanceServer, name string) error {
var op lxd.Operation
var err error
diff --git a/lxc/exec.go b/lxc/exec.go
index 9447848757..31d4df9583 100644
--- a/lxc/exec.go
+++ b/lxc/exec.go
@@ -115,7 +115,7 @@ func (c *cmdExec) Run(cmd *cobra.Command, args []string) error {
return err
}
- d, err := conf.GetContainerServer(remote)
+ d, err := conf.GetInstanceServer(remote)
if err != nil {
return err
}
diff --git a/lxc/export.go b/lxc/export.go
index 6266bbe0b5..29929f3e91 100644
--- a/lxc/export.go
+++ b/lxc/export.go
@@ -58,7 +58,7 @@ func (c *cmdExport) Run(cmd *cobra.Command, args []string) error {
return err
}
- d, err := conf.GetContainerServer(remote)
+ d, err := conf.GetInstanceServer(remote)
if err != nil {
return err
}
diff --git a/lxc/file.go b/lxc/file.go
index 0c03d3c1a6..1945db217b 100644
--- a/lxc/file.go
+++ b/lxc/file.go
@@ -36,7 +36,7 @@ type cmdFile struct {
flagRecursive bool
}
-func fileGetWrapper(server lxd.ContainerServer, container string, path string) (buf io.ReadCloser, resp *lxd.ContainerFileResponse, err error) {
+func fileGetWrapper(server lxd.InstanceServer, container string, path string) (buf io.ReadCloser, resp *lxd.ContainerFileResponse, err error) {
// Signal handling
chSignal := make(chan os.Signal)
signal.Notify(chSignal, os.Interrupt)
@@ -648,7 +648,7 @@ func (c *cmdFilePush) Run(cmd *cobra.Command, args []string) error {
return nil
}
-func (c *cmdFile) recursivePullFile(d lxd.ContainerServer, container string, p string, targetDir string) error {
+func (c *cmdFile) recursivePullFile(d lxd.InstanceServer, container string, p string, targetDir string) error {
buf, resp, err := d.GetContainerFile(container, p)
if err != nil {
return err
@@ -723,7 +723,7 @@ func (c *cmdFile) recursivePullFile(d lxd.ContainerServer, container string, p s
return nil
}
-func (c *cmdFile) recursivePushFile(d lxd.ContainerServer, container string, source string, target string) error {
+func (c *cmdFile) recursivePushFile(d lxd.InstanceServer, container string, source string, target string) error {
source = filepath.Clean(source)
sourceDir, _ := filepath.Split(source)
sourceLen := len(sourceDir)
@@ -821,7 +821,7 @@ func (c *cmdFile) recursivePushFile(d lxd.ContainerServer, container string, sou
return filepath.Walk(source, sendFile)
}
-func (c *cmdFile) recursiveMkdir(d lxd.ContainerServer, container string, p string, mode *os.FileMode, uid int64, gid int64) error {
+func (c *cmdFile) recursiveMkdir(d lxd.InstanceServer, container string, p string, mode *os.FileMode, uid int64, gid int64) error {
/* special case, every container has a /, we don't need to do anything */
if p == "/" {
return nil
diff --git a/lxc/image.go b/lxc/image.go
index 538a8775f6..61c589486b 100644
--- a/lxc/image.go
+++ b/lxc/image.go
@@ -656,7 +656,7 @@ func (c *cmdImageImport) Run(cmd *cobra.Command, args []string) error {
rootfsFile = shared.HostPath(filepath.Clean(rootfsFile))
}
- d, err := conf.GetContainerServer(remote)
+ d, err := conf.GetInstanceServer(remote)
if err != nil {
return err
}
diff --git a/lxc/info.go b/lxc/info.go
index dce77937cf..f33f5fe2bd 100644
--- a/lxc/info.go
+++ b/lxc/info.go
@@ -69,7 +69,7 @@ func (c *cmdInfo) Run(cmd *cobra.Command, args []string) error {
}
}
- d, err := conf.GetContainerServer(remote)
+ d, err := conf.GetInstanceServer(remote)
if err != nil {
return err
}
@@ -302,7 +302,7 @@ func (c *cmdInfo) renderCPU(cpu api.ResourcesCPUSocket, prefix string) {
}
}
-func (c *cmdInfo) remoteInfo(d lxd.ContainerServer) error {
+func (c *cmdInfo) remoteInfo(d lxd.InstanceServer) error {
// Targeting
if c.flagTarget != "" {
if !d.IsClustered() {
@@ -417,7 +417,7 @@ func (c *cmdInfo) remoteInfo(d lxd.ContainerServer) error {
return nil
}
-func (c *cmdInfo) containerInfo(d lxd.ContainerServer, remote config.Remote, name string, showLog bool) error {
+func (c *cmdInfo) containerInfo(d lxd.InstanceServer, remote config.Remote, name string, showLog bool) error {
// Sanity checks
if c.flagTarget != "" {
return fmt.Errorf(i18n.G("--target cannot be used with containers"))
diff --git a/lxc/init.go b/lxc/init.go
index 4586bf0884..65b2b0276d 100644
--- a/lxc/init.go
+++ b/lxc/init.go
@@ -73,7 +73,7 @@ func (c *cmdInit) Run(cmd *cobra.Command, args []string) error {
return err
}
-func (c *cmdInit) create(conf *config.Config, args []string) (lxd.ContainerServer, string, error) {
+func (c *cmdInit) create(conf *config.Config, args []string) (lxd.InstanceServer, string, error) {
var name string
var image string
var remote string
@@ -134,7 +134,7 @@ func (c *cmdInit) create(conf *config.Config, args []string) (lxd.ContainerServe
}
}
- d, err := conf.GetContainerServer(remote)
+ d, err := conf.GetInstanceServer(remote)
if err != nil {
return nil, "", err
}
@@ -325,7 +325,7 @@ func (c *cmdInit) create(conf *config.Config, args []string) (lxd.ContainerServe
return d, name, nil
}
-func (c *cmdInit) guessImage(conf *config.Config, d lxd.ContainerServer, remote string, iremote string, image string) (string, string) {
+func (c *cmdInit) guessImage(conf *config.Config, d lxd.InstanceServer, remote string, iremote string, image string) (string, string) {
if remote != iremote {
return iremote, image
}
@@ -355,7 +355,7 @@ func (c *cmdInit) guessImage(conf *config.Config, d lxd.ContainerServer, remote
return fields[0], fields[1]
}
-func (c *cmdInit) checkNetwork(d lxd.ContainerServer, name string) {
+func (c *cmdInit) checkNetwork(d lxd.InstanceServer, name string) {
ct, _, err := d.GetContainer(name)
if err != nil {
return
diff --git a/lxc/list.go b/lxc/list.go
index d3a484f83d..aaef88c392 100644
--- a/lxc/list.go
+++ b/lxc/list.go
@@ -201,7 +201,7 @@ func (c *cmdList) shouldShow(filters []string, state *api.Container) bool {
return true
}
-func (c *cmdList) listContainers(conf *config.Config, d lxd.ContainerServer, cinfos []api.Container, filters []string, columns []column) error {
+func (c *cmdList) listContainers(conf *config.Config, d lxd.InstanceServer, cinfos []api.Container, filters []string, columns []column) error {
threads := 10
if len(cinfos) < threads {
threads = len(cinfos)
@@ -371,7 +371,7 @@ func (c *cmdList) Run(cmd *cobra.Command, args []string) error {
}
// Connect to LXD
- d, err := conf.GetContainerServer(remote)
+ d, err := conf.GetInstanceServer(remote)
if err != nil {
return err
}
diff --git a/lxc/main.go b/lxc/main.go
index afd68a23eb..c63d040273 100644
--- a/lxc/main.go
+++ b/lxc/main.go
@@ -365,12 +365,12 @@ func (c *cmdGlobal) PostRun(cmd *cobra.Command, args []string) error {
}
type remoteResource struct {
- server lxd.ContainerServer
+ server lxd.InstanceServer
name string
}
func (c *cmdGlobal) ParseServers(remotes ...string) ([]remoteResource, error) {
- servers := map[string]lxd.ContainerServer{}
+ servers := map[string]lxd.InstanceServer{}
resources := []remoteResource{}
for _, remote := range remotes {
@@ -394,7 +394,7 @@ func (c *cmdGlobal) ParseServers(remotes ...string) ([]remoteResource, error) {
}
// New connection
- d, err := c.conf.GetContainerServer(remoteName)
+ d, err := c.conf.GetInstanceServer(remoteName)
if err != nil {
return nil, err
}
diff --git a/lxc/monitor.go b/lxc/monitor.go
index 8c633375f9..22f98cfc7b 100644
--- a/lxc/monitor.go
+++ b/lxc/monitor.go
@@ -74,7 +74,7 @@ func (c *cmdMonitor) Run(cmd *cobra.Command, args []string) error {
}
}
- d, err := conf.GetContainerServer(remote)
+ d, err := conf.GetInstanceServer(remote)
if err != nil {
return err
}
diff --git a/lxc/move.go b/lxc/move.go
index 5985b38932..5d12ec1204 100644
--- a/lxc/move.go
+++ b/lxc/move.go
@@ -108,7 +108,7 @@ func (c *cmdMove) Run(cmd *cobra.Command, args []string) error {
return fmt.Errorf(i18n.G("Can't override configuration or profiles in local rename"))
}
- source, err := conf.GetContainerServer(sourceRemote)
+ source, err := conf.GetInstanceServer(sourceRemote)
if err != nil {
return err
}
@@ -212,7 +212,7 @@ func moveClusterContainer(conf *config.Config, sourceResource, destResource, tar
}
// Connect to the source host
- source, err := conf.GetContainerServer(sourceRemote)
+ source, err := conf.GetInstanceServer(sourceRemote)
if err != nil {
return errors.Wrap(err, i18n.G("Failed to connect to cluster member"))
}
diff --git a/lxc/project.go b/lxc/project.go
index 0624c6fabe..35d2b5af7d 100644
--- a/lxc/project.go
+++ b/lxc/project.go
@@ -693,7 +693,7 @@ func (c *cmdProjectSwitch) Run(cmd *cobra.Command, args []string) error {
}
// Make sure the project exists
- d, err := conf.GetContainerServer(remote)
+ d, err := conf.GetInstanceServer(remote)
if err != nil {
return err
}
diff --git a/lxc/publish.go b/lxc/publish.go
index bba6c5ceb4..3b5031779d 100644
--- a/lxc/publish.go
+++ b/lxc/publish.go
@@ -83,14 +83,14 @@ func (c *cmdPublish) Run(cmd *cobra.Command, args []string) error {
return fmt.Errorf(i18n.G("There is no \"image name\". Did you want an alias?"))
}
- d, err := conf.GetContainerServer(iRemote)
+ d, err := conf.GetInstanceServer(iRemote)
if err != nil {
return err
}
s := d
if cRemote != iRemote {
- s, err = conf.GetContainerServer(cRemote)
+ s, err = conf.GetInstanceServer(cRemote)
if err != nil {
return err
}
diff --git a/lxc/query.go b/lxc/query.go
index 5e8f948a5b..88180c98ed 100644
--- a/lxc/query.go
+++ b/lxc/query.go
@@ -68,7 +68,7 @@ func (c *cmdQuery) Run(cmd *cobra.Command, args []string) error {
}
// Attempt to connect
- d, err := conf.GetContainerServer(remote)
+ d, err := conf.GetInstanceServer(remote)
if err != nil {
return err
}
diff --git a/lxc/remote.go b/lxc/remote.go
index 62dbf0ff63..e6fd440da0 100644
--- a/lxc/remote.go
+++ b/lxc/remote.go
@@ -225,7 +225,7 @@ func (c *cmdRemoteAdd) Run(cmd *cobra.Command, args []string) error {
if c.flagPublic {
d, err = conf.GetImageServer(server)
} else {
- d, err = conf.GetContainerServer(server)
+ d, err = conf.GetInstanceServer(server)
}
// Handle Unix socket connections
@@ -286,7 +286,7 @@ func (c *cmdRemoteAdd) Run(cmd *cobra.Command, args []string) error {
if c.flagPublic {
d, err = conf.GetImageServer(server)
} else {
- d, err = conf.GetContainerServer(server)
+ d, err = conf.GetInstanceServer(server)
}
if err != nil {
@@ -301,11 +301,11 @@ func (c *cmdRemoteAdd) Run(cmd *cobra.Command, args []string) error {
}
if c.flagAuthType == "candid" {
- d.(lxd.ContainerServer).RequireAuthenticated(false)
+ d.(lxd.InstanceServer).RequireAuthenticated(false)
}
// Get server information
- srv, _, err := d.(lxd.ContainerServer).GetServer()
+ srv, _, err := d.(lxd.InstanceServer).GetServer()
if err != nil {
return err
}
@@ -321,14 +321,14 @@ func (c *cmdRemoteAdd) Run(cmd *cobra.Command, args []string) error {
conf.Remotes[server] = remote
// Re-setup the client
- d, err = conf.GetContainerServer(server)
+ d, err = conf.GetInstanceServer(server)
if err != nil {
return err
}
- d.(lxd.ContainerServer).RequireAuthenticated(false)
+ d.(lxd.InstanceServer).RequireAuthenticated(false)
- srv, _, err = d.(lxd.ContainerServer).GetServer()
+ srv, _, err = d.(lxd.InstanceServer).GetServer()
if err != nil {
return err
}
@@ -380,16 +380,16 @@ func (c *cmdRemoteAdd) Run(cmd *cobra.Command, args []string) error {
}
req.Type = "client"
- err = d.(lxd.ContainerServer).CreateCertificate(req)
+ err = d.(lxd.InstanceServer).CreateCertificate(req)
if err != nil {
return err
}
} else {
- d.(lxd.ContainerServer).RequireAuthenticated(true)
+ d.(lxd.InstanceServer).RequireAuthenticated(true)
}
// And check if trusted now
- srv, _, err = d.(lxd.ContainerServer).GetServer()
+ srv, _, err = d.(lxd.InstanceServer).GetServer()
if err != nil {
return err
}
diff --git a/lxc/restore.go b/lxc/restore.go
index 2bbc26065e..acf134cb3d 100644
--- a/lxc/restore.go
+++ b/lxc/restore.go
@@ -53,7 +53,7 @@ func (c *cmdRestore) Run(cmd *cobra.Command, args []string) error {
return err
}
- d, err := conf.GetContainerServer(remote)
+ d, err := conf.GetInstanceServer(remote)
if err != nil {
return err
}
diff --git a/lxc/snapshot.go b/lxc/snapshot.go
index c98db99634..d5b008e16b 100644
--- a/lxc/snapshot.go
+++ b/lxc/snapshot.go
@@ -58,7 +58,7 @@ func (c *cmdSnapshot) Run(cmd *cobra.Command, args []string) error {
return err
}
- d, err := conf.GetContainerServer(remote)
+ d, err := conf.GetInstanceServer(remote)
if err != nil {
return err
}
diff --git a/lxc/utils.go b/lxc/utils.go
index 4de856bc1b..3c866b5c60 100644
--- a/lxc/utils.go
+++ b/lxc/utils.go
@@ -130,7 +130,7 @@ func runBatch(names []string, action func(name string) error) []batchResult {
}
// Add a device to a container
-func containerDeviceAdd(client lxd.ContainerServer, name string, devName string, dev map[string]string) error {
+func containerDeviceAdd(client lxd.InstanceServer, name string, devName string, dev map[string]string) error {
// Get the container entry
container, etag, err := client.GetContainer(name)
if err != nil {
@@ -154,7 +154,7 @@ func containerDeviceAdd(client lxd.ContainerServer, name string, devName string,
}
// Add a device to a profile
-func profileDeviceAdd(client lxd.ContainerServer, name string, devName string, dev map[string]string) error {
+func profileDeviceAdd(client lxd.InstanceServer, name string, devName string, dev map[string]string) error {
// Get the profile entry
profile, profileEtag, err := client.GetProfile(name)
if err != nil {
@@ -179,7 +179,7 @@ func profileDeviceAdd(client lxd.ContainerServer, name string, devName string, d
}
// Create the specified image alises, updating those that already exist
-func ensureImageAliases(client lxd.ContainerServer, aliases []api.ImageAlias, fingerprint string) error {
+func ensureImageAliases(client lxd.InstanceServer, aliases []api.ImageAlias, fingerprint string) error {
if len(aliases) == 0 {
return nil
}
More information about the lxc-devel
mailing list