1
0
mirror of https://github.com/lxc/incus.git synced 2026-02-05 09:46:19 +01:00
Files
incus/client/incus_instances.go

3124 lines
81 KiB
Go
Raw Permalink Normal View History

package incus
import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"path/filepath"
"slices"
"strings"
"github.com/gorilla/websocket"
"github.com/pkg/sftp"
"github.com/lxc/incus/v6/shared/api"
"github.com/lxc/incus/v6/shared/cancel"
"github.com/lxc/incus/v6/shared/ioprogress"
"github.com/lxc/incus/v6/shared/tcp"
localtls "github.com/lxc/incus/v6/shared/tls"
"github.com/lxc/incus/v6/shared/units"
"github.com/lxc/incus/v6/shared/ws"
)
// Instance handling functions.
// instanceTypeToPath converts the instance type to a URL path prefix and query string values.
func (r *ProtocolIncus) instanceTypeToPath(instanceType api.InstanceType) (string, url.Values, error) {
v := url.Values{}
// 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 *ProtocolIncus) GetInstanceNames(instanceType api.InstanceType) ([]string, error) {
baseURL, v, err := r.instanceTypeToPath(instanceType)
if err != nil {
return nil, err
}
// Fetch the raw URL values.
urls := []string{}
_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", baseURL, v.Encode()), nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetInstanceNamesAllProjects returns a list of instance names from all projects.
func (r *ProtocolIncus) GetInstanceNamesAllProjects(instanceType api.InstanceType) (map[string][]string, error) {
instances := []api.Instance{}
path, v, err := r.instanceTypeToPath(instanceType)
if err != nil {
return nil, err
}
v.Set("recursion", "1")
v.Set("all-projects", "true")
// Fetch the raw URL values.
_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &instances)
if err != nil {
return nil, err
}
names := map[string][]string{}
for _, instance := range instances {
names[instance.Project] = append(names[instance.Project], instance.Name)
}
return names, nil
}
// GetInstances returns a list of instances.
func (r *ProtocolIncus) 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
}
// GetInstancesWithFilter returns a filtered list of instances.
func (r *ProtocolIncus) GetInstancesWithFilter(instanceType api.InstanceType, filters []string) ([]api.Instance, error) {
if !r.HasExtension("api_filtering") {
return nil, errors.New("The server is missing the required \"api_filtering\" API extension")
}
instances := []api.Instance{}
path, v, err := r.instanceTypeToPath(instanceType)
if err != nil {
return nil, err
}
v.Set("recursion", "1")
v.Set("filter", parseFilters(filters))
// 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
}
// GetInstancesAllProjects returns a list of instances from all projects.
func (r *ProtocolIncus) GetInstancesAllProjects(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")
v.Set("all-projects", "true")
if !r.HasExtension("instance_all_projects") {
return nil, errors.New("The server is missing the required \"instance_all_projects\" 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
}
// GetInstancesAllProjectsWithFilter returns a filtered list of instances from all projects.
func (r *ProtocolIncus) GetInstancesAllProjectsWithFilter(instanceType api.InstanceType, filters []string) ([]api.Instance, error) {
if !r.HasExtension("api_filtering") {
return nil, errors.New("The server is missing the required \"api_filtering\" API extension")
}
instances := []api.Instance{}
path, v, err := r.instanceTypeToPath(instanceType)
if err != nil {
return nil, err
}
v.Set("recursion", "1")
v.Set("all-projects", "true")
v.Set("filter", parseFilters(filters))
if !r.HasExtension("instance_all_projects") {
return nil, errors.New("The server is missing the required \"instance_all_projects\" 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
}
// UpdateInstances updates all instances to match the requested state.
func (r *ProtocolIncus) UpdateInstances(state api.InstancesPut, ETag string) (Operation, error) {
path, v, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Send the request
op, _, err := r.queryOperation("PUT", fmt.Sprintf("%s?%s", path, v.Encode()), state, ETag)
if err != nil {
return nil, err
}
return op, nil
}
// rebuildInstance initiates a rebuild of a given instance on the Incus Protocol server and returns the corresponding operation or an error.
func (r *ProtocolIncus) rebuildInstance(instanceName string, instance api.InstanceRebuildPost) (Operation, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Send the request
op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/rebuild", path, url.PathEscape(instanceName)), instance, "")
if err != nil {
return nil, err
}
return op, nil
}
// tryRebuildInstance attempts to rebuild a specific instance on multiple target servers identified by their URLs.
// It runs the rebuild process asynchronously and returns a RemoteOperation to monitor the progress and any errors.
func (r *ProtocolIncus) tryRebuildInstance(instanceName string, req api.InstanceRebuildPost, urls []string, op Operation) (RemoteOperation, error) {
if len(urls) == 0 {
return nil, errors.New("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
var errors []remoteOperationResult
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.rebuildInstance(instanceName, req)
if err != nil {
errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})
continue
}
rop.handlerLock.Lock()
rop.targetOp = op
rop.handlerLock.Unlock()
for _, handler := range rop.handlers {
_, _ = rop.targetOp.AddHandler(handler)
}
err = rop.targetOp.Wait()
if err != nil {
errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})
if localtls.IsConnectionError(err) {
continue
}
break
}
success = true
break
}
if !success {
rop.err = remoteOperationError("Failed instance rebuild", errors)
if op != nil {
_ = op.Cancel()
}
}
close(rop.chDone)
}()
return &rop, nil
}
// RebuildInstanceFromImage rebuilds an instance from an image.
func (r *ProtocolIncus) RebuildInstanceFromImage(source ImageServer, image api.Image, instanceName string, req api.InstanceRebuildPost) (RemoteOperation, error) {
err := r.CheckExtension("instances_rebuild")
if err != nil {
return nil, err
}
info, err := r.getSourceImageConnectionInfo(source, image, &req.Source)
if err != nil {
return nil, err
}
if info == nil {
op, err := r.rebuildInstance(instanceName, 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
}
return r.tryRebuildInstance(instanceName, req, info.Addresses, nil)
}
// RebuildInstance rebuilds an instance as empty.
func (r *ProtocolIncus) RebuildInstance(instanceName string, instance api.InstanceRebuildPost) (op Operation, err error) {
err = r.CheckExtension("instances_rebuild")
if err != nil {
return nil, err
}
return r.rebuildInstance(instanceName, instance)
}
// GetInstancesFull returns a list of instances including snapshots, backups and state.
func (r *ProtocolIncus) 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, errors.New("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
}
// GetInstancesFullWithFilter returns a filtered list of instances including snapshots, backups and state.
func (r *ProtocolIncus) GetInstancesFullWithFilter(instanceType api.InstanceType, filters []string) ([]api.InstanceFull, error) {
if !r.HasExtension("api_filtering") {
return nil, errors.New("The server is missing the required \"api_filtering\" API extension")
}
instances := []api.InstanceFull{}
path, v, err := r.instanceTypeToPath(instanceType)
if err != nil {
return nil, err
}
v.Set("recursion", "2")
v.Set("filter", parseFilters(filters))
if !r.HasExtension("container_full") {
return nil, errors.New("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
}
// GetInstancesFullAllProjects returns a list of instances including snapshots, backups and state from all projects.
func (r *ProtocolIncus) GetInstancesFullAllProjects(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")
v.Set("all-projects", "true")
if !r.HasExtension("container_full") {
return nil, errors.New("The server is missing the required \"container_full\" API extension")
}
if !r.HasExtension("instance_all_projects") {
return nil, errors.New("The server is missing the required \"instance_all_projects\" 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
}
// GetInstancesFullAllProjectsWithFilter returns a filtered list of instances including snapshots, backups and state from all projects.
func (r *ProtocolIncus) GetInstancesFullAllProjectsWithFilter(instanceType api.InstanceType, filters []string) ([]api.InstanceFull, error) {
if !r.HasExtension("api_filtering") {
return nil, errors.New("The server is missing the required \"api_filtering\" API extension")
}
instances := []api.InstanceFull{}
path, v, err := r.instanceTypeToPath(instanceType)
if err != nil {
return nil, err
}
v.Set("recursion", "2")
v.Set("all-projects", "true")
v.Set("filter", parseFilters(filters))
if !r.HasExtension("container_full") {
return nil, errors.New("The server is missing the required \"container_full\" API extension")
}
if !r.HasExtension("instance_all_projects") {
return nil, errors.New("The server is missing the required \"instance_all_projects\" 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 *ProtocolIncus) 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
}
// GetInstanceFull returns the instance entry for the provided name along with snapshot information.
func (r *ProtocolIncus) GetInstanceFull(name string) (*api.InstanceFull, string, error) {
instance := api.InstanceFull{}
if !r.HasExtension("instance_get_full") {
// Backward compatibility.
ct, _, err := r.GetInstance(name)
if err != nil {
return nil, "", err
}
cs, _, err := r.GetInstanceState(name)
if err != nil {
return nil, "", err
}
snaps, err := r.GetInstanceSnapshots(name)
if err != nil {
return nil, "", err
}
backups, err := r.GetInstanceBackups(name)
if err != nil {
return nil, "", err
}
instance.Instance = *ct
instance.State = cs
instance.Snapshots = snaps
instance.Backups = backups
return &instance, "", nil
}
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?recursion=1", 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 *ProtocolIncus) CreateInstanceFromBackup(args InstanceBackupArgs) (Operation, error) {
if !r.HasExtension("container_backup") {
return nil, errors.New("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 == "" && args.Name == "" && args.Config == nil && args.Devices == nil {
// Send the request
op, _, err := r.queryOperation("POST", path, args.BackupFile, "")
if err != nil {
return nil, err
}
return op, nil
}
if args.PoolName != "" && !r.HasExtension("container_backup_override_pool") {
return nil, errors.New(`The server is missing the required "container_backup_override_pool" API extension`)
}
if args.Name != "" && !r.HasExtension("backup_override_name") {
return nil, errors.New(`The server is missing the required "backup_override_name" API extension`)
}
if (args.Config != nil || args.Devices != nil) && !r.HasExtension("backup_override_config") {
return nil, errors.New(`The server is missing the required "backup_override_config" API extension`)
}
// Prepare the HTTP request
reqURL, err := r.setQueryAttributes(fmt.Sprintf("%s/1.0%s", r.httpBaseURL.String(), 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")
if args.PoolName != "" {
req.Header.Set("X-Incus-pool", args.PoolName)
}
if args.Name != "" {
req.Header.Set("X-Incus-name", args.Name)
}
if args.Config != nil {
configOverride := strings.Join(args.Config, " ")
req.Header.Set("X-Incus-config", configOverride)
}
if args.Devices != nil {
devicesOverride := strings.Join(args.Devices, " ")
req.Header.Set("X-Incus-devices", devicesOverride)
}
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return nil, err
}
defer func() { _ = resp.Body.Close() }()
// Handle errors
response, _, err := incusParseResponse(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 Incus creates a new instance.
func (r *ProtocolIncus) CreateInstance(instance api.InstancesPost) (Operation, error) {
path, _, err := r.instanceTypeToPath(instance.Type)
if err != nil {
return nil, err
}
if instance.Source.InstanceOnly {
if !r.HasExtension("container_only_migration") {
return nil, errors.New("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
}
// tryCreateInstance attempts to create a new instance on multiple target servers specified by their URLs.
// It runs the instance creation asynchronously and returns a RemoteOperation to monitor the progress and any errors.
func (r *ProtocolIncus) tryCreateInstance(req api.InstancesPost, urls []string, op Operation) (RemoteOperation, error) {
if len(urls) == 0 {
return nil, errors.New("The source server isn't listening on the network")
}
rop := remoteOperation{
chDone: make(chan bool),
}
operation := req.Source.Operation
// Forward targetOp to remote op
chConnect := make(chan error, 1)
chWait := make(chan error, 1)
go func() {
success := false
var errors []remoteOperationResult
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 = append(errors, remoteOperationResult{URL: serverURL, Error: err})
continue
}
rop.handlerLock.Lock()
rop.targetOp = op
rop.handlerLock.Unlock()
for _, handler := range rop.handlers {
_, _ = rop.targetOp.AddHandler(handler)
}
err = rop.targetOp.Wait()
if err != nil {
errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})
if localtls.IsConnectionError(err) {
continue
}
break
}
success = true
break
}
if success {
chConnect <- nil
close(chConnect)
} else {
chConnect <- remoteOperationError("Failed instance creation", errors)
close(chConnect)
if op != nil {
_ = op.Cancel()
}
}
}()
if op != nil {
go func() {
chWait <- op.Wait()
close(chWait)
}()
}
go func() {
var err error
select {
case err = <-chConnect:
case err = <-chWait:
}
rop.err = err
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 *ProtocolIncus) CreateInstanceFromImage(source ImageServer, image api.Image, req api.InstancesPost) (RemoteOperation, error) {
info, err := r.getSourceImageConnectionInfo(source, image, &req.Source)
if err != nil {
return nil, err
}
// If the source server is the same as the target server, create the instance directly.
if info == nil {
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
}
return r.tryCreateInstance(req, info.Addresses, nil)
}
// CopyInstance copies a instance from a remote server. Additional options can be passed using InstanceCopyArgs.
func (r *ProtocolIncus) CopyInstance(source InstanceServer, instance api.Instance, args *InstanceCopyArgs) (RemoteOperation, error) {
// Base request
req := api.InstancesPost{
Name: instance.Name,
InstancePut: instance.Writable(),
Type: api.InstanceType(instance.Type),
}
req.Source.BaseImage = instance.Config["volatile.base_image"]
// Process the copy arguments
if args != nil {
// Quick checks.
if args.InstanceOnly {
if !r.HasExtension("container_only_migration") {
return nil, errors.New("The target server is missing the required \"container_only_migration\" API extension")
}
if !source.HasExtension("container_only_migration") {
return nil, errors.New("The source server is missing the required \"container_only_migration\" API extension")
}
}
if slices.Contains([]string{"push", "relay"}, args.Mode) {
if !r.HasExtension("container_push") {
return nil, errors.New("The target server is missing the required \"container_push\" API extension")
}
if !source.HasExtension("container_push") {
return nil, errors.New("The source server is missing the required \"container_push\" API extension")
}
}
if args.Mode == "push" && !source.HasExtension("container_push_target") {
return nil, errors.New("The source server is missing the required \"container_push_target\" API extension")
}
if args.Refresh {
if !r.HasExtension("container_incremental_copy") {
return nil, errors.New("The target server is missing the required \"container_incremental_copy\" API extension")
}
if !source.HasExtension("container_incremental_copy") {
return nil, errors.New("The source server is missing the required \"container_incremental_copy\" API extension")
}
}
if args.RefreshExcludeOlder && !source.HasExtension("custom_volume_refresh_exclude_older_snapshots") {
return nil, errors.New("The source server is missing the required \"custom_volume_refresh_exclude_older_snapshots\" API extension")
}
if args.AllowInconsistent {
if !r.HasExtension("instance_allow_inconsistent_copy") {
return nil, errors.New("The source server is missing the required \"instance_allow_inconsistent_copy\" API extension")
}
}
// Allow overriding the target name
if args.Name != "" {
req.Name = args.Name
}
req.Source.Live = args.Live
req.Source.InstanceOnly = args.InstanceOnly
req.Source.Refresh = args.Refresh
req.Source.RefreshExcludeOlder = args.RefreshExcludeOlder
req.Source.AllowInconsistent = args.AllowInconsistent
}
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: %w", err)
}
destInfo, err := r.GetConnectionInfo()
if err != nil {
return nil, fmt.Errorf("Failed to get destination connection info: %w", 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, errors.New("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,
InstanceOnly: req.Source.InstanceOnly,
AllowInconsistent: req.Source.AllowInconsistent,
}
// 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
req.Source.RefreshExcludeOlder = args.RefreshExcludeOlder
op, err := r.CreateInstance(req)
if err != nil {
return nil, err
}
opAPI := op.Get()
targetSecrets := map[string]string{}
for k, v := range opAPI.Metadata {
val, ok := v.(string)
if ok {
targetSecrets[k] = val
}
}
// 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, op)
}
// 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 {
val, ok := v.(string)
if ok {
sourceSecrets[k] = val
}
}
// 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 {
val, ok := v.(string)
if ok {
targetSecrets[k] = val
}
}
// 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, op)
}
// UpdateInstance updates the instance definition.
func (r *ProtocolIncus) 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 Incus renames the instance.
func (r *ProtocolIncus) RenameInstance(name string, instance api.InstancePost) (Operation, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Quick check.
if instance.Migration {
return nil, errors.New("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
}
// tryMigrateInstance attempts to migrate a specific instance from a source server to one of the target URLs.
// The function runs the migration operation asynchronously and returns a RemoteOperation to track the progress and handle any errors.
func (r *ProtocolIncus) tryMigrateInstance(source InstanceServer, name string, req api.InstancePost, urls []string, op Operation) (RemoteOperation, error) {
if len(urls) == 0 {
return nil, errors.New("The target server isn't listening on the network")
}
rop := remoteOperation{
chDone: make(chan bool),
}
operation := req.Target.Operation
// Forward targetOp to remote op
chConnect := make(chan error, 1)
chWait := make(chan error, 1)
go func() {
success := false
var errors []remoteOperationResult
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 = append(errors, remoteOperationResult{URL: serverURL, Error: err})
continue
}
rop.targetOp = op
for _, handler := range rop.handlers {
_, _ = rop.targetOp.AddHandler(handler)
}
err = rop.targetOp.Wait()
if err != nil {
errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})
if localtls.IsConnectionError(err) {
continue
}
break
}
success = true
break
}
if success {
chConnect <- nil
close(chConnect)
} else {
chConnect <- remoteOperationError("Failed instance migration", errors)
close(chConnect)
if op != nil {
_ = op.Cancel()
}
}
}()
if op != nil {
go func() {
chWait <- op.Wait()
close(chWait)
}()
}
go func() {
var err error
select {
case err = <-chConnect:
case err = <-chWait:
}
rop.err = err
close(rop.chDone)
}()
return &rop, nil
}
// MigrateInstance requests that Incus prepares for a instance migration.
func (r *ProtocolIncus) MigrateInstance(name string, instance api.InstancePost) (Operation, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
if instance.InstanceOnly {
if !r.HasExtension("container_only_migration") {
return nil, errors.New("The server is missing the required \"container_only_migration\" API extension")
}
}
if instance.Pool != "" && !r.HasExtension("instance_pool_move") {
return nil, errors.New("The server is missing the required \"instance_pool_move\" API extension")
}
if instance.Project != "" && !r.HasExtension("instance_project_move") {
return nil, errors.New("The server is missing the required \"instance_project_move\" API extension")
}
if instance.AllowInconsistent && !r.HasExtension("cluster_migration_inconsistent_copy") {
return nil, errors.New("The server is missing the required \"cluster_migration_inconsistent_copy\" API extension")
}
// Quick check.
if !instance.Migration {
return nil, errors.New("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 Incus deletes the instance.
func (r *ProtocolIncus) 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 Incus spawns a command inside the instance.
func (r *ProtocolIncus) ExecInstance(instanceName string, exec api.InstanceExecPost, args *InstanceExecArgs) (Operation, error) {
// Ensure args are equivalent to empty InstanceExecArgs.
if args == nil {
args = &InstanceExecArgs{}
}
if exec.RecordOutput {
if !r.HasExtension("container_exec_recording") {
return nil, errors.New("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, errors.New("The server is missing the required \"container_exec_user_group_cwd\" API extension")
}
}
var uri string
if r.IsAgent() {
uri = "/exec"
} else {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
uri = fmt.Sprintf("%s/%s/exec", path, url.PathEscape(instanceName))
}
// Send the request
op, _, err := r.queryOperation("POST", uri, exec, "")
if err != nil {
return nil, err
}
opAPI := op.Get()
// Process additional arguments
// Parse the fds
fds := map[string]string{}
value, ok := opAPI.Metadata["fds"]
if ok {
values, ok := value.(map[string]any)
if ok {
for k, v := range values {
val, ok := v.(string)
if ok {
fds[k] = val
}
}
}
}
if exec.RecordOutput && (args.Stdout != nil || args.Stderr != nil) {
err = op.Wait()
if err != nil {
return nil, err
}
opAPI = op.Get()
outputFiles := map[string]string{}
outputs, ok := opAPI.Metadata["output"].(map[string]any)
if ok {
for k, v := range outputs {
val, ok := v.(string)
if ok {
outputFiles[k] = val
}
}
}
if outputFiles["1"] != "" {
reader, _ := r.getInstanceExecOutputLogFile(instanceName, filepath.Base(outputFiles["1"]))
if args.Stdout != nil {
_, errCopy := io.Copy(args.Stdout, reader)
// Regardless of errCopy value, we want to delete the file after a copy operation
errDelete := r.deleteInstanceExecOutputLogFile(instanceName, filepath.Base(outputFiles["1"]))
if errDelete != nil {
return nil, errDelete
}
if errCopy != nil {
return nil, fmt.Errorf("Could not copy the content of the exec output log file to stdout: %w", err)
}
}
err = r.deleteInstanceExecOutputLogFile(instanceName, filepath.Base(outputFiles["1"]))
if err != nil {
return nil, err
}
}
if outputFiles["2"] != "" {
reader, _ := r.getInstanceExecOutputLogFile(instanceName, filepath.Base(outputFiles["2"]))
if args.Stderr != nil {
_, errCopy := io.Copy(args.Stderr, reader)
errDelete := r.deleteInstanceExecOutputLogFile(instanceName, filepath.Base(outputFiles["1"]))
if errDelete != nil {
return nil, errDelete
}
if errCopy != nil {
return nil, fmt.Errorf("Could not copy the content of the exec output log file to stderr: %w", err)
}
}
err = r.deleteInstanceExecOutputLogFile(instanceName, filepath.Base(outputFiles["2"]))
if err != nil {
return nil, err
}
}
}
if fds[api.SecretNameControl] != "" {
conn, err := r.GetOperationWebsocket(opAPI.ID, fds[api.SecretNameControl])
if err != nil {
return nil, err
}
go func() {
_, _, _ = conn.ReadMessage() // Consume pings from server.
}()
if args.Control != nil {
// Call the control handler with a connection to the control socket
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() {
ws.MirrorRead(conn, args.Stdin)
<-ws.MirrorWrite(conn, args.Stdout)
_ = conn.Close()
if args.DataDone != nil {
close(args.DataDone)
}
}()
} else {
if args.DataDone != nil {
close(args.DataDone)
}
}
} else {
// Handle non-interactive sessions
dones := make(map[int]chan error)
conns := []*websocket.Conn{}
// Handle stdin
if fds["0"] != "" {
conn, err := r.GetOperationWebsocket(opAPI.ID, fds["0"])
if err != nil {
return nil, err
}
go func() {
_, _, _ = conn.ReadMessage() // Consume pings from server.
}()
conns = append(conns, conn)
dones[0] = ws.MirrorRead(conn, args.Stdin)
}
waitConns := 0 // Used for keeping track of when stdout and stderr have finished.
// Handle stdout
if fds["1"] != "" {
conn, err := r.GetOperationWebsocket(opAPI.ID, fds["1"])
if err != nil {
return nil, err
}
// Discard Stdout from remote command if output writer not supplied.
if args.Stdout == nil {
args.Stdout = io.Discard
}
conns = append(conns, conn)
dones[1] = ws.MirrorWrite(conn, args.Stdout)
waitConns++
}
// Handle stderr
if fds["2"] != "" {
conn, err := r.GetOperationWebsocket(opAPI.ID, fds["2"])
if err != nil {
return nil, err
}
// Discard Stderr from remote command if output writer not supplied.
if args.Stderr == nil {
args.Stderr = io.Discard
}
conns = append(conns, conn)
dones[2] = ws.MirrorWrite(conn, args.Stderr)
waitConns++
}
// Wait for everything to be done
go func() {
for {
select {
case <-dones[0]:
// Handle stdin finish, but don't wait for it if output channels
// have all finished.
dones[0] = nil
_ = conns[0].Close()
case <-dones[1]:
dones[1] = nil
_ = conns[1].Close()
waitConns--
case <-dones[2]:
dones[2] = nil
_ = conns[2].Close()
waitConns--
}
if waitConns <= 0 {
// Close stdin websocket if defined and not already closed.
if dones[0] != nil {
conns[0].Close()
}
break
}
}
if args.DataDone != nil {
close(args.DataDone)
}
}()
}
return op, nil
}
// GetInstanceFile retrieves the provided path from the instance.
func (r *ProtocolIncus) GetInstanceFile(instanceName string, filePath string) (io.ReadCloser, *InstanceFileResponse, error) {
var err error
var requestURL string
urlEncode := func(path string, query map[string]string) (string, error) {
u, err := url.Parse(path)
if err != nil {
return "", err
}
params := url.Values{}
for key, value := range query {
params.Add(key, value)
}
u.RawQuery = params.Encode()
return u.String(), nil
}
if r.IsAgent() {
requestURL, err = urlEncode(
fmt.Sprintf("%s/1.0/files", r.httpBaseURL.String()),
map[string]string{"path": filePath})
} else {
var path string
path, _, err = r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, nil, err
}
// Prepare the HTTP request
requestURL, err = urlEncode(
fmt.Sprintf("%s/1.0%s/%s/files", r.httpBaseURL.String(), path, url.PathEscape(instanceName)),
map[string]string{"path": filePath})
}
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
}
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return nil, nil, err
}
// Check the return value for a cleaner error
if resp.StatusCode != http.StatusOK {
_, _, err := incusParseResponse(resp)
if err != nil {
return nil, nil, err
}
}
// Parse the headers
uid, gid, mode, fileType, _ := api.ParseFileHeaders(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 Incus to create a file in the instance.
func (r *ProtocolIncus) CreateInstanceFile(instanceName string, filePath string, args InstanceFileArgs) error {
if args.Type == "directory" {
if !r.HasExtension("directory_manipulation") {
return errors.New("The server is missing the required \"directory_manipulation\" API extension")
}
}
if args.Type == "symlink" {
if !r.HasExtension("file_symlinks") {
return errors.New("The server is missing the required \"file_symlinks\" API extension")
}
}
if args.WriteMode == "append" {
if !r.HasExtension("file_append") {
return errors.New("The server is missing the required \"file_append\" API extension")
}
}
var requestURL string
if r.IsAgent() {
requestURL = fmt.Sprintf("%s/1.0/files?path=%s", r.httpBaseURL.String(), url.QueryEscape(filePath))
} else {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return err
}
// Prepare the HTTP request
requestURL = fmt.Sprintf("%s/1.0%s/%s/files?path=%s", r.httpBaseURL.String(), path, url.PathEscape(instanceName), url.QueryEscape(filePath))
}
requestURL, err := r.setQueryAttributes(requestURL)
if err != nil {
return err
}
req, err := http.NewRequest("POST", requestURL, args.Content)
if err != nil {
return err
}
req.GetBody = func() (io.ReadCloser, error) {
_, err := args.Content.Seek(0, 0)
if err != nil {
return nil, err
}
return io.NopCloser(args.Content), nil
}
// Set the various headers
if args.UID > -1 {
req.Header.Set("X-Incus-uid", fmt.Sprintf("%d", args.UID))
}
if args.GID > -1 {
req.Header.Set("X-Incus-gid", fmt.Sprintf("%d", args.GID))
}
if args.Mode > -1 {
req.Header.Set("X-Incus-mode", fmt.Sprintf("%04o", args.Mode))
}
if args.Type != "" {
req.Header.Set("X-Incus-type", args.Type)
}
if args.WriteMode != "" {
req.Header.Set("X-Incus-write", args.WriteMode)
}
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return err
}
// Check the return value for a cleaner error
_, _, err = incusParseResponse(resp)
if err != nil {
return err
}
return nil
}
// DeleteInstanceFile deletes a file in the instance.
func (r *ProtocolIncus) DeleteInstanceFile(instanceName string, filePath string) error {
if !r.HasExtension("file_delete") {
return errors.New("The server is missing the required \"file_delete\" API extension")
}
var requestURL string
if r.IsAgent() {
requestURL = fmt.Sprintf("/files?path=%s", url.QueryEscape(filePath))
} else {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return err
}
// Prepare the HTTP request
requestURL = fmt.Sprintf("%s/%s/files?path=%s", path, url.PathEscape(instanceName), url.QueryEscape(filePath))
}
requestURL, err := r.setQueryAttributes(requestURL)
if err != nil {
return err
}
// Send the request
_, _, err = r.query("DELETE", requestURL, nil, "")
if err != nil {
return err
}
return nil
}
// rawSFTPConn connects to the apiURL, upgrades to an SFTP raw connection and returns it.
func (r *ProtocolIncus) rawSFTPConn(apiURL *url.URL) (net.Conn, error) {
// Get the HTTP transport.
httpTransport, err := r.getUnderlyingHTTPTransport()
if err != nil {
return nil, err
}
req := &http.Request{
Method: http.MethodGet,
URL: apiURL,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(http.Header),
Host: apiURL.Host,
}
req.Header["Upgrade"] = []string{"sftp"}
req.Header["Connection"] = []string{"Upgrade"}
r.addClientHeaders(req)
// Establish the connection.
var conn net.Conn
if httpTransport.TLSClientConfig != nil {
conn, err = httpTransport.DialTLSContext(context.Background(), "tcp", apiURL.Host)
} else {
conn, err = httpTransport.DialContext(context.Background(), "tcp", apiURL.Host)
}
if err != nil {
return nil, err
}
remoteTCP, _ := tcp.ExtractConn(conn)
if remoteTCP != nil {
err = tcp.SetTimeouts(remoteTCP, 0)
if err != nil {
return nil, err
}
}
err = req.Write(conn)
if err != nil {
return nil, err
}
resp, err := http.ReadResponse(bufio.NewReader(conn), req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusSwitchingProtocols {
_, _, err := incusParseResponse(resp)
if err != nil {
return nil, err
}
}
if resp.Header.Get("Upgrade") != "sftp" {
return nil, errors.New("Missing or unexpected Upgrade header in response")
}
return conn, err
}
// GetInstanceFileSFTPConn returns a connection to the instance's SFTP endpoint.
func (r *ProtocolIncus) GetInstanceFileSFTPConn(instanceName string) (net.Conn, error) {
apiURL := api.NewURL()
apiURL.URL = r.httpBaseURL // Preload the URL with the client base URL.
apiURL.Path("1.0", "instances", instanceName, "sftp")
r.setURLQueryAttributes(&apiURL.URL)
return r.rawSFTPConn(&apiURL.URL)
}
// GetInstanceFileSFTP returns an SFTP connection to the instance.
func (r *ProtocolIncus) GetInstanceFileSFTP(instanceName string) (*sftp.Client, error) {
conn, err := r.GetInstanceFileSFTPConn(instanceName)
if err != nil {
return nil, err
}
// Get a SFTP client.
client, err := sftp.NewClientPipe(conn, conn, sftp.MaxPacketUnchecked(128*1024))
if err != nil {
_ = conn.Close()
return nil, err
}
go func() {
// Wait for the client to be done before closing the connection.
_ = client.Wait()
_ = conn.Close()
}()
return client, nil
}
// GetInstanceSnapshotNames returns a list of snapshot names for the instance.
func (r *ProtocolIncus) GetInstanceSnapshotNames(instanceName string) ([]string, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Fetch the raw URL values.
urls := []string{}
baseURL := fmt.Sprintf("%s/%s/snapshots", path, url.PathEscape(instanceName))
_, err = r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetInstanceSnapshots returns a list of snapshots for the instance.
func (r *ProtocolIncus) 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 *ProtocolIncus) 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 Incus creates a new snapshot for the instance.
func (r *ProtocolIncus) 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, errors.New("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 *ProtocolIncus) CopyInstanceSnapshot(source InstanceServer, instanceName string, snapshot api.InstanceSnapshot, args *InstanceSnapshotCopyArgs) (RemoteOperation, error) {
// Backward compatibility (with broken Name field)
fields := strings.Split(snapshot.Name, "/")
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, errors.New("The server is missing the required \"container_snapshot_stateful_migration\" API extension")
}
req.Stateful = snapshot.Stateful
req.Source.Live = false // Snapshots are never running and so we don't need live migration.
}
req.Source.BaseImage = snapshot.Config["volatile.base_image"]
// Process the copy arguments
if args != nil {
// Quick checks.
if slices.Contains([]string{"push", "relay"}, args.Mode) {
if !r.HasExtension("container_push") {
return nil, errors.New("The target server is missing the required \"container_push\" API extension")
}
if !source.HasExtension("container_push") {
return nil, errors.New("The source server is missing the required \"container_push\" API extension")
}
}
if args.Mode == "push" && !source.HasExtension("container_push_target") {
return nil, errors.New("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: %w", err)
}
destInfo, err := r.GetConnectionInfo()
if err != nil {
return nil, fmt.Errorf("Failed to get destination connection info: %w", err)
}
instance, _, err := source.GetInstance(cName)
if err != nil {
return nil, fmt.Errorf("Failed to get instance info: %w", 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, errors.New("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
}
// If deadling with migration, we need to set the type.
if source.HasExtension("virtual-machines") {
inst, _, err := source.GetInstance(instanceName)
if err != nil {
return nil, err
}
req.Type = api.InstanceType(inst.Type)
}
// 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 {
val, ok := v.(string)
if ok {
targetSecrets[k] = val
}
}
// 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 {
val, ok := v.(string)
if ok {
sourceSecrets[k] = val
}
}
// 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 {
val, ok := v.(string)
if ok {
targetSecrets[k] = val
}
}
// 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, op)
}
// RenameInstanceSnapshot requests that Incus renames the snapshot.
func (r *ProtocolIncus) RenameInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPost) (Operation, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Quick check.
if instance.Migration {
return nil, errors.New("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 *ProtocolIncus) tryMigrateInstanceSnapshot(source InstanceServer, instanceName string, name string, req api.InstanceSnapshotPost, urls []string) (RemoteOperation, error) {
if len(urls) == 0 {
return nil, errors.New("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
var errors []remoteOperationResult
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 = append(errors, remoteOperationResult{URL: serverURL, Error: err})
continue
}
rop.targetOp = op
for _, handler := range rop.handlers {
_, _ = rop.targetOp.AddHandler(handler)
}
err = rop.targetOp.Wait()
if err != nil {
errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})
if localtls.IsConnectionError(err) {
continue
}
break
}
success = true
break
}
if !success {
rop.err = remoteOperationError("Failed instance migration", errors)
}
close(rop.chDone)
}()
return &rop, nil
}
// MigrateInstanceSnapshot requests that Incus prepares for a snapshot migration.
func (r *ProtocolIncus) MigrateInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPost) (Operation, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Quick check.
if !instance.Migration {
return nil, errors.New("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 Incus deletes the instance snapshot.
func (r *ProtocolIncus) 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 Incus updates the instance snapshot.
func (r *ProtocolIncus) 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, errors.New("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 *ProtocolIncus) GetInstanceState(name string) (*api.InstanceState, string, error) {
var uri string
if r.IsAgent() {
uri = "/state"
} else {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, "", err
}
uri = fmt.Sprintf("%s/%s/state", path, url.PathEscape(name))
}
state := api.InstanceState{}
// Fetch the raw value
etag, err := r.queryStruct("GET", uri, nil, "", &state)
if err != nil {
return nil, "", err
}
return &state, etag, nil
}
// UpdateInstanceState updates the instance to match the requested state.
func (r *ProtocolIncus) 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
}
// GetInstanceAccess returns an Access entry for the provided instance name.
func (r *ProtocolIncus) GetInstanceAccess(name string) (api.Access, error) {
access := api.Access{}
if !r.HasExtension("instance_access") {
return nil, errors.New("The server is missing the required \"instance_access\" API extension")
}
// Fetch the raw value
_, err := r.queryStruct("GET", fmt.Sprintf("/instances/%s/access", url.PathEscape(name)), nil, "", &access)
if err != nil {
return nil, err
}
return access, nil
}
// GetInstanceLogfiles returns a list of logfiles for the instance.
func (r *ProtocolIncus) GetInstanceLogfiles(name string) ([]string, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Fetch the raw URL values.
urls := []string{}
baseURL := fmt.Sprintf("%s/%s/logs", path, url.PathEscape(name))
_, err = r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetInstanceLogfile returns the content of the requested logfile.
//
// Note that it's the caller's responsibility to close the returned ReadCloser.
func (r *ProtocolIncus) GetInstanceLogfile(name string, filename string) (io.ReadCloser, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Prepare the HTTP request
uri := fmt.Sprintf("%s/1.0%s/%s/logs/%s", r.httpBaseURL.String(), path, url.PathEscape(name), url.PathEscape(filename))
uri, err = r.setQueryAttributes(uri)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
return nil, err
}
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return nil, err
}
// Check the return value for a cleaner error
if resp.StatusCode != http.StatusOK {
_, _, err := incusParseResponse(resp)
if err != nil {
return nil, err
}
}
return resp.Body, err
}
// DeleteInstanceLogfile deletes the requested logfile.
func (r *ProtocolIncus) 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
}
// getInstanceExecOutputLogFile returns the content of the requested exec logfile.
//
// Note that it's the caller's responsibility to close the returned ReadCloser.
func (r *ProtocolIncus) getInstanceExecOutputLogFile(name string, filename string) (io.ReadCloser, error) {
err := r.CheckExtension("container_exec_recording")
if err != nil {
return nil, err
}
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Prepare the HTTP request
uri := fmt.Sprintf("%s/1.0%s/%s/logs/exec-output/%s", r.httpBaseURL.String(), path, url.PathEscape(name), url.PathEscape(filename))
uri, err = r.setQueryAttributes(uri)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
return nil, err
}
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return nil, err
}
// Check the return value for a cleaner error
if resp.StatusCode != http.StatusOK {
_, _, err := incusParseResponse(resp)
if err != nil {
return nil, err
}
}
return resp.Body, nil
}
// deleteInstanceExecOutputLogFiles deletes the requested exec logfile.
func (r *ProtocolIncus) deleteInstanceExecOutputLogFile(instanceName string, filename string) error {
err := r.CheckExtension("container_exec_recording")
if err != nil {
return err
}
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return err
}
// Send the request
_, _, err = r.query("DELETE", fmt.Sprintf("%s/%s/logs/exec-output/%s", path, url.PathEscape(instanceName), url.PathEscape(filename)), nil, "")
if err != nil {
return err
}
return nil
}
// GetInstanceMetadata returns instance metadata.
func (r *ProtocolIncus) 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, "", errors.New("The server is missing the required \"container_edit_metadata\" API extension")
}
metadata := api.ImageMetadata{}
uri := fmt.Sprintf("%s/%s/metadata", path, url.PathEscape(name))
etag, err := r.queryStruct("GET", uri, nil, "", &metadata)
if err != nil {
return nil, "", err
}
return &metadata, etag, err
}
// UpdateInstanceMetadata sets the content of the instance metadata file.
func (r *ProtocolIncus) UpdateInstanceMetadata(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 errors.New("The server is missing the required \"container_edit_metadata\" API extension")
}
uri := fmt.Sprintf("%s/%s/metadata", path, url.PathEscape(name))
_, _, err = r.query("PUT", uri, metadata, ETag)
if err != nil {
return err
}
return nil
}
// GetInstanceTemplateFiles returns the list of names of template files for a instance.
func (r *ProtocolIncus) 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, errors.New("The server is missing the required \"container_edit_metadata\" API extension")
}
templates := []string{}
uri := fmt.Sprintf("%s/%s/metadata/templates", path, url.PathEscape(instanceName))
_, err = r.queryStruct("GET", uri, nil, "", &templates)
if err != nil {
return nil, err
}
return templates, nil
}
// GetInstanceTemplateFile returns the content of a template file for a instance.
func (r *ProtocolIncus) 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, errors.New("The server is missing the required \"container_edit_metadata\" API extension")
}
uri := fmt.Sprintf("%s/1.0%s/%s/metadata/templates?path=%s", r.httpBaseURL.String(), path, url.PathEscape(instanceName), url.QueryEscape(templateName))
uri, err = r.setQueryAttributes(uri)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
return nil, err
}
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return nil, err
}
// Check the return value for a cleaner error
if resp.StatusCode != http.StatusOK {
_, _, err := incusParseResponse(resp)
if err != nil {
return nil, err
}
}
return resp.Body, err
}
// CreateInstanceTemplateFile creates an a template for a instance.
func (r *ProtocolIncus) CreateInstanceTemplateFile(instanceName string, templateName string, content io.ReadSeeker) error {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return err
}
if !r.HasExtension("container_edit_metadata") {
return errors.New("The server is missing the required \"container_edit_metadata\" API extension")
}
uri := fmt.Sprintf("%s/1.0%s/%s/metadata/templates?path=%s", r.httpBaseURL.String(), path, url.PathEscape(instanceName), url.QueryEscape(templateName))
uri, err = r.setQueryAttributes(uri)
if err != nil {
return err
}
req, err := http.NewRequest("POST", uri, content)
if err != nil {
return err
}
req.GetBody = func() (io.ReadCloser, error) {
_, err := content.Seek(0, 0)
if err != nil {
return nil, err
}
return io.NopCloser(content), nil
}
req.Header.Set("Content-Type", "application/octet-stream")
// Send the request
resp, err := r.DoHTTP(req)
// Check the return value for a cleaner error
if resp.StatusCode != http.StatusOK {
_, _, err := incusParseResponse(resp)
if err != nil {
return err
}
}
return err
}
// DeleteInstanceTemplateFile deletes a template file for a instance.
func (r *ProtocolIncus) DeleteInstanceTemplateFile(name string, templateName string) error {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return err
}
if !r.HasExtension("container_edit_metadata") {
return errors.New("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 Incus attaches to the console device of a instance.
func (r *ProtocolIncus) ConsoleInstance(instanceName string, console api.InstanceConsolePost, args *InstanceConsoleArgs) (Operation, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
if !r.HasExtension("console") {
return nil, errors.New("The server is missing the required \"console\" API extension")
}
if console.Type == "" {
console.Type = "console"
}
if console.Type == "vga" && !r.HasExtension("console_vga_type") {
return nil, errors.New("The server is missing the required \"console_vga_type\" API extension")
}
if console.Force && !r.HasExtension("console_force") {
return nil, errors.New(`The server is missing the required "console_force" 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, errors.New("A terminal must be set")
}
if args.Control == nil {
return nil, errors.New("A control channel must be set")
}
// Parse the fds
fds := map[string]string{}
value, ok := opAPI.Metadata["fds"]
if ok {
values, ok := value.(map[string]any)
if ok {
for k, v := range values {
val, ok := v.(string)
if ok {
fds[k] = val
}
}
}
}
var controlConn *websocket.Conn
// Call the control handler with a connection to the control socket
if fds[api.SecretNameControl] == "" {
return nil, errors.New("Did not receive a file descriptor for the control channel")
}
controlConn, err = r.GetOperationWebsocket(opAPI.ID, fds[api.SecretNameControl])
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() {
_, writeDone := ws.Mirror(conn, args.Terminal)
<-writeDone
_ = conn.Close()
}()
return op, nil
}
// ConsoleInstanceDynamic requests that Incus attaches to the console device of a
// instance with the possibility of opening multiple connections to it.
//
// Every time the returned 'console' function is called, a new connection will
// be established and proxied to the given io.ReadWriteCloser.
func (r *ProtocolIncus) ConsoleInstanceDynamic(instanceName string, console api.InstanceConsolePost, args *InstanceConsoleArgs) (Operation, func(io.ReadWriteCloser) error, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, nil, err
}
if !r.HasExtension("console") {
return nil, nil, errors.New("The server is missing the required \"console\" API extension")
}
if console.Type == "" {
console.Type = "console"
}
if console.Type == "vga" && !r.HasExtension("console_vga_type") {
return nil, nil, errors.New("The server is missing the required \"console_vga_type\" API extension")
}
if console.Force && !r.HasExtension("console_force") {
return nil, nil, errors.New(`The server is missing the required "console_force" 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, nil, err
}
opAPI := op.Get()
if args == nil {
return nil, nil, errors.New("No arguments provided")
}
if args.Control == nil {
return nil, nil, errors.New("A control channel must be set")
}
// Parse the fds.
fds := map[string]string{}
value, ok := opAPI.Metadata["fds"]
if ok {
values, ok := value.(map[string]any)
if ok {
for k, v := range values {
val, ok := v.(string)
if ok {
fds[k] = val
}
}
}
}
// Call the control handler with a connection to the control socket.
if fds[api.SecretNameControl] == "" {
return nil, nil, errors.New("Did not receive a file descriptor for the control channel")
}
controlConn, err := r.GetOperationWebsocket(opAPI.ID, fds[api.SecretNameControl])
if err != nil {
return nil, nil, err
}
go args.Control(controlConn)
// Handle main disconnect.
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)
f := func(rwc io.ReadWriteCloser) error {
// Connect to the websocket.
conn, err := r.GetOperationWebsocket(opAPI.ID, fds["0"])
if err != nil {
return err
}
// Attach reader/writer.
_, writeDone := ws.Mirror(conn, rwc)
<-writeDone
_ = conn.Close()
return nil
}
return op, f, nil
}
// GetInstanceConsoleLog requests that Incus attaches to the console device of a instance.
//
// Note that it's the caller's responsibility to close the returned ReadCloser.
func (r *ProtocolIncus) GetInstanceConsoleLog(instanceName string, _ *InstanceConsoleLogArgs) (io.ReadCloser, error) {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
if !r.HasExtension("console") {
return nil, errors.New("The server is missing the required \"console\" API extension")
}
// Prepare the HTTP request
uri := fmt.Sprintf("%s/1.0%s/%s/console", r.httpBaseURL.String(), path, url.PathEscape(instanceName))
uri, err = r.setQueryAttributes(uri)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
return nil, err
}
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return nil, err
}
// Check the return value for a cleaner error
if resp.StatusCode != http.StatusOK {
_, _, err := incusParseResponse(resp)
if err != nil {
return nil, err
}
}
return resp.Body, err
}
// DeleteInstanceConsoleLog deletes the requested instance's console log.
func (r *ProtocolIncus) DeleteInstanceConsoleLog(instanceName string, _ *InstanceConsoleLogArgs) error {
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return err
}
if !r.HasExtension("console") {
return errors.New("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 *ProtocolIncus) GetInstanceBackupNames(instanceName string) ([]string, error) {
if !r.HasExtension("container_backup") {
return nil, errors.New("The server is missing the required \"container_backup\" API extension")
}
path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
if err != nil {
return nil, err
}
// Fetch the raw URL values.
urls := []string{}
baseURL := fmt.Sprintf("%s/%s/backups", path, url.PathEscape(instanceName))
_, err = r.queryStruct("GET", baseURL, nil, "", &urls)
if err != nil {
return nil, err
}
// Parse it.
return urlsToResourceNames(baseURL, urls...)
}
// GetInstanceBackups returns a list of backups for the instance.
func (r *ProtocolIncus) 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, errors.New("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 *ProtocolIncus) 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, "", errors.New("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 Incus creates a new backup for the instance.
func (r *ProtocolIncus) 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, errors.New("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 Incus renames the backup.
func (r *ProtocolIncus) 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, errors.New("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 Incus deletes the instance backup.
func (r *ProtocolIncus) 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, errors.New("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 *ProtocolIncus) 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, errors.New("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.httpBaseURL.String(), 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.DoHTTP, request)
if err != nil {
return nil, err
}
defer func() { _ = response.Body.Close() }()
defer close(doneCh)
if response.StatusCode != http.StatusOK {
_, _, err := incusParseResponse(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
}
func (r *ProtocolIncus) proxyMigration(targetOp *operation, targetSecrets map[string]string, source InstanceServer, sourceOp *operation, sourceSecrets map[string]string) error {
// Quick 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[api.SecretNameControl] == "" {
return errors.New("Migration target didn't setup the required \"control\" socket")
}
// Struct used to hold everything together
type proxy struct {
done chan struct{}
sourceConn *websocket.Conn
targetConn *websocket.Conn
}
proxies := map[string]*proxy{}
// Connect the control socket
sourceConn, err := source.GetOperationWebsocket(sourceOp.ID, sourceSecrets[api.SecretNameControl])
if err != nil {
return err
}
targetConn, err := r.GetOperationWebsocket(targetOp.ID, targetSecrets[api.SecretNameControl])
if err != nil {
return err
}
proxies[api.SecretNameControl] = &proxy{
done: ws.Proxy(sourceConn, targetConn),
sourceConn: sourceConn,
targetConn: targetConn,
}
// Connect the data sockets
for name := range sourceSecrets {
if name == api.SecretNameControl {
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: ws.Proxy(sourceConn, targetConn),
}
}
// Cleanup once everything is done
go func() {
// Wait for control socket
<-proxies[api.SecretNameControl].done
_ = proxies[api.SecretNameControl].sourceConn.Close()
_ = proxies[api.SecretNameControl].targetConn.Close()
// Then deal with the others
for name, proxy := range proxies {
if name == api.SecretNameControl {
continue
}
<-proxy.done
_ = proxy.sourceConn.Close()
_ = proxy.targetConn.Close()
}
}()
return nil
}
// GetInstanceDebugMemory retrieves memory debug information for a given instance and saves it to the specified file path.
func (r *ProtocolIncus) GetInstanceDebugMemory(name string, format string) (io.ReadCloser, error) {
path, v, err := r.instanceTypeToPath(api.InstanceTypeVM)
if err != nil {
return nil, err
}
v.Set("format", format)
// Prepare the HTTP request
requestURL := fmt.Sprintf("%s/1.0%s/%s/debug/memory?%s", r.httpBaseURL.String(), path, url.PathEscape(name), v.Encode())
requestURL, err = r.setQueryAttributes(requestURL)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", requestURL, nil)
if err != nil {
return nil, err
}
// Send the request
resp, err := r.DoHTTP(req)
if err != nil {
return nil, err
}
// Check the return value for a cleaner error
if resp.StatusCode != http.StatusOK {
_, _, err := incusParseResponse(resp)
if err != nil {
return nil, err
}
}
return resp.Body, nil
}