1
0
mirror of https://github.com/lxc/incus.git synced 2026-02-06 03:46:32 +01:00
Files
incus/internal/server/instance/instance_utils.go
2025-10-28 16:30:04 +00:00

1325 lines
38 KiB
Go

package instance
import (
"bytes"
"context"
"crypto/rand"
"database/sql"
"errors"
"fmt"
"math/big"
"net/http"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"time"
"github.com/flosch/pongo2/v6"
"github.com/google/uuid"
incus "github.com/lxc/incus/v6/client"
"github.com/lxc/incus/v6/internal/instance"
"github.com/lxc/incus/v6/internal/migration"
"github.com/lxc/incus/v6/internal/server/backup"
"github.com/lxc/incus/v6/internal/server/db"
"github.com/lxc/incus/v6/internal/server/db/cluster"
deviceConfig "github.com/lxc/incus/v6/internal/server/device/config"
"github.com/lxc/incus/v6/internal/server/instance/drivers/qemudefault"
"github.com/lxc/incus/v6/internal/server/instance/instancetype"
"github.com/lxc/incus/v6/internal/server/instance/operationlock"
"github.com/lxc/incus/v6/internal/server/operations"
"github.com/lxc/incus/v6/internal/server/seccomp"
"github.com/lxc/incus/v6/internal/server/state"
storageDrivers "github.com/lxc/incus/v6/internal/server/storage/drivers"
"github.com/lxc/incus/v6/internal/server/sys"
localUtil "github.com/lxc/incus/v6/internal/server/util"
internalUtil "github.com/lxc/incus/v6/internal/util"
"github.com/lxc/incus/v6/internal/version"
"github.com/lxc/incus/v6/shared/api"
"github.com/lxc/incus/v6/shared/idmap"
"github.com/lxc/incus/v6/shared/logger"
"github.com/lxc/incus/v6/shared/osarch"
"github.com/lxc/incus/v6/shared/resources"
"github.com/lxc/incus/v6/shared/revert"
"github.com/lxc/incus/v6/shared/units"
"github.com/lxc/incus/v6/shared/util"
"github.com/lxc/incus/v6/shared/validate"
)
// ValidDevices is linked from instance/drivers.validDevices to validate device config.
var ValidDevices func(state *state.State, p api.Project, instanceType instancetype.Type, localDevices deviceConfig.Devices, expandedDevices deviceConfig.Devices) error
// Load is linked from instance/drivers.load to allow different instance types to be loaded.
var Load func(s *state.State, args db.InstanceArgs, p api.Project) (Instance, error)
// Create is linked from instance/drivers.create to allow difference instance types to be created.
// Returns a revert fail function that can be used to undo this function if a subsequent step fails.
var Create func(s *state.State, args db.InstanceArgs, p api.Project, op *operations.Operation) (Instance, revert.Hook, error)
func exclusiveConfigKeys(key1 string, key2 string, config map[string]string) (val string, ok bool, err error) {
if config[key1] != "" && config[key2] != "" {
return "", false, fmt.Errorf("Mutually exclusive keys %s and %s are set", key1, key2)
}
val, ok = config[key1]
if ok {
return
}
val, ok = config[key2]
if ok {
return
}
return "", false, nil
}
// ValidConfig validates an instance's config.
func ValidConfig(sysOS *sys.OS, config map[string]string, expanded bool, instanceType instancetype.Type) error {
if config == nil {
return nil
}
for k, v := range config {
if instanceType == instancetype.Any && !expanded && strings.HasPrefix(k, instance.ConfigVolatilePrefix) {
return errors.New("Volatile keys can only be set on instances")
}
if instanceType == instancetype.Any && !expanded && strings.HasPrefix(k, "image.") {
return errors.New("Image keys can only be set on instances")
}
err := validConfigKey(sysOS, k, v, instanceType)
if err != nil {
return err
}
after, ok := strings.CutPrefix(k, "systemd.credential.")
if ok && config["systemd.credential-binary."+after] != "" {
return fmt.Errorf("Mutually exclusive keys %s and systemd.credential-binary.%s are set", k, after)
}
}
_, rawSeccomp := config["raw.seccomp"]
_, isAllow, err := exclusiveConfigKeys("security.syscalls.allow", "security.syscalls.whitelist", config)
if err != nil {
return err
}
_, isDeny, err := exclusiveConfigKeys("security.syscalls.deny", "security.syscalls.blacklist", config)
if err != nil {
return err
}
val, _, err := exclusiveConfigKeys("security.syscalls.deny_default", "security.syscalls.blacklist_default", config)
if err != nil {
return err
}
isDenyDefault := util.IsTrue(val)
val, _, err = exclusiveConfigKeys("security.syscalls.deny_compat", "security.syscalls.blacklist_compat", config)
if err != nil {
return err
}
isDenyCompat := util.IsTrue(val)
if rawSeccomp && (isAllow || isDeny || isDenyDefault || isDenyCompat) {
return errors.New("raw.seccomp is mutually exclusive with security.syscalls*")
}
if isAllow && (isDeny || isDenyDefault || isDenyCompat) {
return errors.New("security.syscalls.allow is mutually exclusive with security.syscalls.deny*")
}
_, err = seccomp.SyscallInterceptMountFilter(config)
if err != nil {
return err
}
if instanceType == instancetype.Container && expanded && util.IsFalseOrEmpty(config["security.privileged"]) && sysOS.IdmapSet == nil {
return errors.New("No uid/gid allocation configured. In this mode, only privileged containers are supported")
}
if util.IsTrue(config["security.privileged"]) && util.IsTrue(config["nvidia.runtime"]) {
return errors.New("nvidia.runtime is incompatible with privileged containers")
}
return nil
}
func validConfigKey(os *sys.OS, key string, value string, instanceType instancetype.Type) error {
f, err := instance.ConfigKeyChecker(key, instanceType.ToAPI())
if err != nil {
return err
}
if err = f(value); err != nil {
return err
}
if strings.HasPrefix(key, "limits.kernel.") && instanceType == instancetype.VM {
return fmt.Errorf("%s isn't supported for VMs", key)
}
if key == "raw.lxc" {
return lxcValidConfig(value)
}
if key == "security.syscalls.deny_compat" || key == "security.syscalls.blacklist_compat" {
for _, arch := range os.Architectures {
if arch == osarch.ARCH_64BIT_INTEL_X86 ||
arch == osarch.ARCH_64BIT_ARMV8_LITTLE_ENDIAN ||
arch == osarch.ARCH_64BIT_POWERPC_BIG_ENDIAN {
return nil
}
}
return fmt.Errorf("%s isn't supported on this architecture", key)
}
return nil
}
func lxcParseRawLXC(line string) (string, string, error) {
// Ignore empty lines
if len(line) == 0 {
return "", "", nil
}
// Skip space {"\t", " "}
line = strings.TrimLeft(line, "\t ")
// Ignore comments
if strings.HasPrefix(line, "#") {
return "", "", nil
}
// Ensure the format is valid
membs := strings.SplitN(line, "=", 2)
if len(membs) != 2 {
return "", "", fmt.Errorf("Invalid raw.lxc line: %s", line)
}
key := strings.ToLower(strings.Trim(membs[0], " \t"))
val := strings.Trim(membs[1], " \t")
return key, val, nil
}
func lxcValidConfig(rawLxc string) error {
for _, line := range strings.Split(rawLxc, "\n") {
key, _, err := lxcParseRawLXC(line)
if err != nil {
return err
}
if key == "" {
continue
}
// block some keys
if key == "lxc.logfile" || key == "lxc.log.file" {
return errors.New("Setting lxc.logfile is not allowed")
}
if key == "lxc.syslog" || key == "lxc.log.syslog" {
return errors.New("Setting lxc.log.syslog is not allowed")
}
if key == "lxc.ephemeral" {
return errors.New("Setting lxc.ephemeral is not allowed")
}
if strings.HasPrefix(key, "lxc.prlimit.") {
return fmt.Errorf(`Process limits should be set via ` +
`"limits.kernel.[limit name]" and not ` +
`directly via "lxc.prlimit.[limit name]"`)
}
}
return nil
}
// AllowedUnprivilegedOnlyMap checks that root user is not mapped into instance.
func AllowedUnprivilegedOnlyMap(rawIdmap string) error {
rawMaps, err := idmap.NewSetFromIncusIDMap(rawIdmap)
if err != nil {
return err
}
for _, ent := range rawMaps.Entries {
if ent.HostID == 0 {
return errors.New("Cannot map root user into container as the server was configured to only allow unprivileged containers")
}
}
return nil
}
// LoadByID loads an instance by ID.
func LoadByID(s *state.State, id int) (Instance, error) {
var project string
var name string
err := s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
var err error
// Get the DB record
project, name, err = tx.GetInstanceProjectAndName(ctx, id)
if err != nil {
return fmt.Errorf("Failed getting instance project and name: %w", err)
}
return nil
})
if err != nil {
return nil, err
}
return LoadByProjectAndName(s, project, name)
}
// LoadInstanceDatabaseObject loads a db.Instance object, accounting for snapshots.
func LoadInstanceDatabaseObject(ctx context.Context, tx *db.ClusterTx, project, name string) (*cluster.Instance, error) {
var container *cluster.Instance
var err error
if strings.Contains(name, instance.SnapshotDelimiter) {
parts := strings.SplitN(name, instance.SnapshotDelimiter, 2)
instanceName := parts[0]
snapshotName := parts[1]
instance, err := cluster.GetInstance(ctx, tx.Tx(), project, instanceName)
if err != nil {
return nil, fmt.Errorf("Failed to fetch instance %q in project %q: %w", name, project, err)
}
snapshot, err := cluster.GetInstanceSnapshot(ctx, tx.Tx(), project, instanceName, snapshotName)
if err != nil {
return nil, fmt.Errorf("Failed to fetch snapshot %q of instance %q in project %q: %w", snapshotName, instanceName, project, err)
}
c := snapshot.ToInstance(instance.Name, instance.Node, instance.Type, instance.Architecture)
container = &c
} else {
container, err = cluster.GetInstance(ctx, tx.Tx(), project, name)
if err != nil {
return nil, fmt.Errorf("Failed to fetch instance %q in project %q: %w", name, project, err)
}
}
return container, nil
}
// LoadByProjectAndName loads an instance by project and name.
func LoadByProjectAndName(s *state.State, projectName string, instanceName string) (Instance, error) {
// Get the DB record
var args db.InstanceArgs
var p *api.Project
err := s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
proj, err := cluster.GetProject(ctx, tx.Tx(), projectName)
if err != nil {
return err
}
p, err = proj.ToAPI(ctx, tx.Tx())
if err != nil {
return err
}
inst, err := LoadInstanceDatabaseObject(ctx, tx, projectName, instanceName)
if err != nil {
return err
}
instArgs, err := tx.InstancesToInstanceArgs(ctx, true, *inst)
if err != nil {
return err
}
args = instArgs[inst.ID]
return nil
})
if err != nil {
return nil, err
}
inst, err := Load(s, args, *p)
if err != nil {
return nil, fmt.Errorf("Failed to load instance: %w", err)
}
return inst, nil
}
// LoadNodeAll loads all instances on this server.
func LoadNodeAll(s *state.State, instanceType instancetype.Type) ([]Instance, error) {
var err error
var instances []Instance
filter := cluster.InstanceFilter{Type: instanceType.Filter()}
if s.ServerName != "" {
filter.Node = &s.ServerName
}
err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
return tx.InstanceList(ctx, func(dbInst db.InstanceArgs, p api.Project) error {
inst, err := Load(s, dbInst, p)
if err != nil {
return fmt.Errorf("Failed loading instance %q in project %q: %w", dbInst.Name, dbInst.Project, err)
}
instances = append(instances, inst)
return nil
}, filter)
})
if err != nil {
return nil, err
}
return instances, nil
}
// LoadFromBackup loads from a mounted instance's backup file.
// If applyProfiles is false, then the profiles property will be cleared to prevent profile enrichment from DB.
// Then the expanded config and expanded devices from the backup file will be applied to the local config and
// local devices respectively. This is done to allow an expanded instance to be returned without needing the DB.
func LoadFromBackup(s *state.State, projectName string, instancePath string, applyProfiles bool) (Instance, error) {
var inst Instance
backupYamlPath := filepath.Join(instancePath, "backup.yaml")
backupConf, err := backup.ParseConfigYamlFile(backupYamlPath)
if err != nil {
return nil, fmt.Errorf("Failed parsing instance backup file from %q: %w", backupYamlPath, err)
}
instDBArgs, err := backup.ConfigToInstanceDBArgs(s, backupConf, projectName, applyProfiles)
if err != nil {
return nil, err
}
if !applyProfiles {
// Stop instance.Load() from expanding profile config from DB, and apply expanded config from
// backup file to local config. This way we can still see the devices even if DB not available.
instDBArgs.Config = backupConf.Container.ExpandedConfig
instDBArgs.Devices = deviceConfig.NewDevices(backupConf.Container.ExpandedDevices)
}
var p *api.Project
err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
proj, err := cluster.GetProject(ctx, tx.Tx(), projectName)
if err != nil {
return err
}
p, err = proj.ToAPI(ctx, tx.Tx())
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
inst, err = Load(s, *instDBArgs, *p)
if err != nil {
return nil, fmt.Errorf("Failed loading instance from backup file %q: %w", backupYamlPath, err)
}
return inst, nil
}
// DeviceNextInterfaceHWAddr generates a random MAC address.
func DeviceNextInterfaceHWAddr(pattern string) (string, error) {
// Generate a new random MAC address using the given pattern.
ret := bytes.Buffer{}
for _, c := range pattern {
if c == 'x' || c == 'X' {
c, err := rand.Int(rand.Reader, big.NewInt(16))
if err != nil {
return "", err
}
ret.WriteString(fmt.Sprintf("%x", c.Int64()))
} else {
ret.WriteString(string(c))
}
}
return ret.String(), nil
}
// BackupLoadByName load an instance backup from the database.
func BackupLoadByName(s *state.State, project, name string) (*backup.InstanceBackup, error) {
var args db.InstanceBackup
// Get the backup database record
err := s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
var err error
args, err = tx.GetInstanceBackup(ctx, project, name)
return err
})
if err != nil {
return nil, err
}
// Load the instance it belongs to
instance, err := LoadByID(s, args.InstanceID)
if err != nil {
return nil, err
}
return backup.NewInstanceBackup(s, instance, args.ID, name, args.CreationDate, args.ExpiryDate, args.InstanceOnly, args.OptimizedStorage), nil
}
// ResolveImage takes an instance source and returns a hash suitable for instance creation or download.
func ResolveImage(ctx context.Context, tx *db.ClusterTx, projectName string, source api.InstanceSource) (string, error) {
if source.Fingerprint != "" {
return source.Fingerprint, nil
}
if source.Alias != "" {
if source.Server != "" {
return source.Alias, nil
}
_, alias, err := tx.GetImageAlias(ctx, projectName, source.Alias, true)
if err != nil {
return "", err
}
return alias.Target, nil
}
if source.Properties != nil {
if source.Server != "" {
return "", errors.New("Property match is only supported for local images")
}
hashes, err := tx.GetImagesFingerprints(ctx, projectName, false)
if err != nil {
return "", err
}
var image *api.Image
for _, imageHash := range hashes {
_, img, err := tx.GetImageByFingerprintPrefix(ctx, imageHash, cluster.ImageFilter{Project: &projectName})
if err != nil {
continue
}
if image != nil && img.CreatedAt.Before(image.CreatedAt) {
continue
}
match := true
for key, value := range source.Properties {
if img.Properties[key] != value {
match = false
break
}
}
if !match {
continue
}
image = img
}
if image != nil {
return image.Fingerprint, nil
}
return "", errors.New("No matching image could be found")
}
return "", errors.New("Must specify one of alias, fingerprint or properties for init from image")
}
// SuitableArchitectures returns a slice of architecture ids based on an instance create request.
//
// An empty list indicates that the request may be handled by any architecture.
// A nil list indicates that we can't tell at this stage, typically for private images.
func SuitableArchitectures(ctx context.Context, s *state.State, tx *db.ClusterTx, projectName string, sourceInst *cluster.Instance, sourceImageRef string, req api.InstancesPost) ([]int, error) {
// Handle cases where the architecture is already provided.
if slices.Contains([]string{"migration", "none"}, req.Source.Type) && req.Architecture != "" {
id, err := osarch.ArchitectureID(req.Architecture)
if err != nil {
return nil, err
}
return []int{id}, nil
}
// For migration, an architecture must be specified in the req.
if req.Source.Type == "migration" && req.Architecture == "" {
return nil, api.StatusErrorf(http.StatusBadRequest, "An architecture must be specified in migration requests")
}
// For none, allow any architecture.
if req.Source.Type == "none" {
return []int{}, nil
}
// For copy, always use the source architecture.
if req.Source.Type == "copy" {
return []int{sourceInst.Architecture}, nil
}
// For image, things get a bit more complicated.
if req.Source.Type == "image" {
// Handle local images.
if req.Source.Server == "" {
_, img, err := tx.GetImageByFingerprintPrefix(ctx, sourceImageRef, cluster.ImageFilter{Project: &projectName})
if err != nil {
return nil, err
}
id, err := osarch.ArchitectureID(img.Architecture)
if err != nil {
return nil, err
}
return []int{id}, nil
}
// Handle remote images.
if req.Source.Server != "" {
if req.Source.Secret != "" {
// We can't retrieve a private image, defer to later processing.
return nil, nil
}
var err error
var remote incus.ImageServer
if slices.Contains([]string{"", "incus", "lxd"}, req.Source.Protocol) {
// Remote image server.
remote, err = incus.ConnectPublicIncus(req.Source.Server, &incus.ConnectionArgs{
TLSServerCert: req.Source.Certificate,
UserAgent: version.UserAgent,
Proxy: s.Proxy,
CachePath: s.OS.CacheDir,
CacheExpiry: time.Hour,
SkipGetEvents: true,
SkipGetServer: true,
})
if err != nil {
return nil, err
}
} else if req.Source.Protocol == "simplestreams" {
// Remote simplestreams image server.
remote, err = incus.ConnectSimpleStreams(req.Source.Server, &incus.ConnectionArgs{
TLSServerCert: req.Source.Certificate,
UserAgent: version.UserAgent,
Proxy: s.Proxy,
CachePath: s.OS.CacheDir,
CacheExpiry: time.Hour,
})
if err != nil {
return nil, err
}
} else if req.Source.Protocol == "oci" {
// Remote OCI registry.
remote, err = incus.ConnectOCI(req.Source.Server, &incus.ConnectionArgs{
TLSServerCert: req.Source.Certificate,
UserAgent: version.UserAgent,
Proxy: s.Proxy,
CachePath: s.OS.CacheDir,
CacheExpiry: time.Hour,
})
if err != nil {
return nil, err
}
} else {
return nil, api.StatusErrorf(http.StatusBadRequest, "Unsupported remote image server protocol %q", req.Source.Protocol)
}
// Look for a matching alias.
entries, err := remote.GetImageAliasArchitectures(string(req.Type), sourceImageRef)
if err != nil {
// Look for a matching image by fingerprint.
img, _, err := remote.GetImage(sourceImageRef)
if err != nil {
return nil, err
}
id, err := osarch.ArchitectureID(img.Architecture)
if err != nil {
return nil, err
}
return []int{id}, nil
}
architectures := []int{}
for arch := range entries {
id, err := osarch.ArchitectureID(arch)
if err != nil {
return nil, err
}
architectures = append(architectures, id)
}
return architectures, nil
}
}
// No other known types
return nil, api.StatusErrorf(http.StatusBadRequest, "Unknown instance source type %q", req.Source.Type)
}
// ValidName validates an instance name. There are different validation rules for instance snapshot names
// so it takes an argument indicating whether the name is to be used for a snapshot or not.
func ValidName(instanceName string, isSnapshot bool) error {
if isSnapshot {
parentName, snapshotName, _ := api.GetParentAndSnapshotName(instanceName)
err := validate.IsHostname(parentName)
if err != nil {
return fmt.Errorf("Invalid instance name: %w", err)
}
// Snapshot part is more flexible, but doesn't allow space or / character.
if strings.ContainsAny(snapshotName, " /") {
return errors.New("Invalid instance snapshot name: Cannot contain space or / characters")
}
} else {
if strings.Contains(instanceName, instance.SnapshotDelimiter) {
return fmt.Errorf("The character %q is reserved for snapshots", instance.SnapshotDelimiter)
}
err := validate.IsHostname(instanceName)
if err != nil {
return fmt.Errorf("Invalid instance name: %w", err)
}
}
return nil
}
// CreateInternal creates an instance record and storage volume record in the database and sets up devices.
// Accepts a reverter that revert steps this function does will be added to. It is up to the caller to
// call the revert's Fail() or Success() function as needed.
// Returns the created instance, along with a "create" operation lock that needs to be marked as Done once the
// instance is fully completed, and a revert fail function that can be used to undo this function if a subsequent
// step fails.
func CreateInternal(s *state.State, args db.InstanceArgs, op *operations.Operation, clearLogDir bool, checkArchitecture bool) (Instance, *operationlock.InstanceOperation, revert.Hook, error) {
reverter := revert.New()
defer reverter.Fail()
// Check instance type requested is supported by this machine.
err := s.InstanceTypes[args.Type]
if err != nil {
return nil, nil, nil, fmt.Errorf("Instance type %q is not supported on this server: %w", args.Type, err)
}
// Set default values.
if args.Project == "" {
args.Project = api.ProjectDefaultName
}
if args.Profiles == nil {
err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
args.Profiles, err = tx.GetProfiles(ctx, args.Project, []string{"default"})
return err
})
if err != nil {
return nil, nil, nil, errors.New("Failed to get default profile for new instance")
}
}
if args.Config == nil {
args.Config = map[string]string{}
}
if args.BaseImage != "" {
args.Config["volatile.base_image"] = args.BaseImage
}
if args.Config["volatile.uuid"] == "" {
args.Config["volatile.uuid"] = uuid.New().String()
}
args.Config["volatile.uuid.generation"] = args.Config["volatile.uuid"]
if args.Devices == nil {
args.Devices = deviceConfig.Devices{}
}
if args.Architecture == 0 {
args.Architecture = s.OS.Architectures[0]
}
err = ValidName(args.Name, args.Snapshot)
if err != nil {
return nil, nil, nil, err
}
if !args.Snapshot {
// Unset expiry date since instances don't expire.
args.ExpiryDate = time.Time{}
// Generate a cloud-init instance-id if not provided.
//
// This is generated here rather than in startCommon as only new
// instances or those which get modified should receive an instance-id.
// Existing instances will keep using their instance name as instance-id to
// avoid triggering cloud-init on upgrade.
if args.Config["volatile.cloud-init.instance-id"] == "" {
args.Config["volatile.cloud-init.instance-id"] = uuid.New().String()
}
}
// Validate instance config.
err = ValidConfig(s.OS, args.Config, false, args.Type)
if err != nil {
return nil, nil, nil, err
}
// Leave validating devices to Create function call below.
// Validate architecture.
_, err = osarch.ArchitectureName(args.Architecture)
if err != nil {
return nil, nil, nil, err
}
if checkArchitecture && !slices.Contains(s.OS.Architectures, args.Architecture) {
return nil, nil, nil, errors.New("Requested architecture isn't supported by this host")
}
var profiles []string
err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
// Validate profiles.
profiles, err = tx.GetProfileNames(ctx, args.Project)
return err
})
if err != nil {
return nil, nil, nil, err
}
checkedProfiles := map[string]bool{}
for _, profile := range args.Profiles {
if !slices.Contains(profiles, profile.Name) {
return nil, nil, nil, fmt.Errorf("Requested profile %q doesn't exist", profile.Name)
}
if checkedProfiles[profile.Name] {
return nil, nil, nil, errors.New("Duplicate profile found in request")
}
checkedProfiles[profile.Name] = true
}
if args.CreationDate.IsZero() {
args.CreationDate = time.Now().UTC()
}
if args.LastUsedDate.IsZero() {
args.LastUsedDate = time.Unix(0, 0).UTC()
}
// Prevent concurrent create requests for same instance.
opl, err := operationlock.Create(args.Project, args.Name, op, operationlock.ActionCreate, false, false)
if err != nil {
return nil, nil, nil, err
}
reverter.Add(func() { opl.Done(err) })
var dbInst cluster.Instance
var p *api.Project
err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
proj, err := cluster.GetProject(ctx, tx.Tx(), args.Project)
if err != nil {
return err
}
p, err = proj.ToAPI(ctx, tx.Tx())
if err != nil {
return err
}
devices, err := cluster.APIToDevices(args.Devices.CloneNative())
if err != nil {
return err
}
if args.Snapshot {
parts := strings.SplitN(args.Name, instance.SnapshotDelimiter, 2)
instanceName := parts[0]
snapshotName := parts[1]
instance, err := cluster.GetInstance(ctx, tx.Tx(), args.Project, instanceName)
if err != nil {
return fmt.Errorf("Get instance %q in project %q", instanceName, args.Project)
}
snapshot := cluster.InstanceSnapshot{
Project: args.Project,
Instance: instanceName,
Name: snapshotName,
CreationDate: args.CreationDate,
Stateful: args.Stateful,
Description: args.Description,
ExpiryDate: sql.NullTime{Time: args.ExpiryDate, Valid: true},
}
id, err := cluster.CreateInstanceSnapshot(ctx, tx.Tx(), snapshot)
if err != nil {
return fmt.Errorf("Add snapshot info to the database: %w", err)
}
err = cluster.CreateInstanceSnapshotConfig(ctx, tx.Tx(), id, args.Config)
if err != nil {
return err
}
err = cluster.CreateInstanceSnapshotDevices(ctx, tx.Tx(), id, devices)
if err != nil {
return err
}
// Read back the snapshot, to get ID and creation time.
s, err := cluster.GetInstanceSnapshot(ctx, tx.Tx(), args.Project, instanceName, snapshotName)
if err != nil {
return fmt.Errorf("Fetch created snapshot from the database: %w", err)
}
dbInst = s.ToInstance(instance.Name, instance.Node, instance.Type, instance.Architecture)
newArgs, err := tx.InstancesToInstanceArgs(ctx, false, dbInst)
if err != nil {
return err
}
// Populate profile info that was already loaded.
newInstArgs := newArgs[dbInst.ID]
newInstArgs.Profiles = args.Profiles
args = newInstArgs
return nil
}
// Create the instance entry.
dbInst = cluster.Instance{
Project: args.Project,
Name: args.Name,
Node: s.ServerName,
Type: args.Type,
Snapshot: args.Snapshot,
Architecture: args.Architecture,
Ephemeral: args.Ephemeral,
CreationDate: args.CreationDate,
Stateful: args.Stateful,
LastUseDate: sql.NullTime{Time: args.LastUsedDate, Valid: true},
Description: args.Description,
ExpiryDate: sql.NullTime{Time: args.ExpiryDate, Valid: true},
}
instanceID, err := cluster.CreateInstance(ctx, tx.Tx(), dbInst)
if err != nil {
return fmt.Errorf("Add instance info to the database: %w", err)
}
err = cluster.CreateInstanceDevices(ctx, tx.Tx(), instanceID, devices)
if err != nil {
return err
}
err = cluster.CreateInstanceConfig(ctx, tx.Tx(), instanceID, args.Config)
if err != nil {
return err
}
profileNames := make([]string, 0, len(args.Profiles))
for _, profile := range args.Profiles {
profileNames = append(profileNames, profile.Name)
}
err = cluster.UpdateInstanceProfiles(ctx, tx.Tx(), int(instanceID), dbInst.Project, profileNames)
if err != nil {
return err
}
// Read back the instance, to get ID and creation time.
dbRow, err := cluster.GetInstance(ctx, tx.Tx(), args.Project, args.Name)
if err != nil {
return fmt.Errorf("Fetch created instance from the database: %w", err)
}
dbInst = *dbRow
if dbInst.ID < 1 {
return fmt.Errorf("Unexpected instance database ID %d: %w", dbInst.ID, err)
}
newArgs, err := tx.InstancesToInstanceArgs(ctx, false, dbInst)
if err != nil {
return err
}
// Populate profile info that was already loaded.
newInstArgs := newArgs[dbInst.ID]
newInstArgs.Profiles = args.Profiles
args = newInstArgs
return nil
})
if err != nil {
if errors.Is(err, db.ErrAlreadyDefined) {
thing := "Instance"
if instance.IsSnapshot(args.Name) {
thing = "Snapshot"
}
return nil, nil, nil, fmt.Errorf("%s %q already exists", thing, args.Name)
}
return nil, nil, nil, err
}
reverter.Add(func() {
_ = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
return tx.DeleteInstance(ctx, dbInst.Project, dbInst.Name)
})
})
inst, cleanup, err := Create(s, args, *p, op)
if err != nil {
logger.Error("Failed initializing instance", logger.Ctx{"project": args.Project, "instance": args.Name, "type": args.Type, "err": err})
return nil, nil, nil, fmt.Errorf("Failed initializing instance: %w", err)
}
reverter.Add(cleanup)
// Wipe any existing log for this instance name.
if clearLogDir {
_ = os.RemoveAll(inst.LogPath())
_ = os.RemoveAll(inst.RunPath())
}
cleanup = reverter.Clone().Fail
reverter.Success()
return inst, opl, cleanup, err
}
// NextSnapshotName finds the next snapshot for an instance.
func NextSnapshotName(s *state.State, inst Instance, defaultPattern string) (string, error) {
var err error
pattern := inst.ExpandedConfig()["snapshots.pattern"]
if pattern == "" {
pattern = defaultPattern
}
pattern, err = internalUtil.RenderTemplate(pattern, pongo2.Context{
"creation_date": time.Now(),
})
if err != nil {
return "", err
}
count := strings.Count(pattern, "%d")
if count > 1 {
return "", fmt.Errorf("Snapshot pattern may contain '%%d' only once")
} else if count == 1 {
var i int
_ = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
i = tx.GetNextInstanceSnapshotIndex(ctx, inst.Project().Name, inst.Name(), pattern)
return nil
})
return strings.Replace(pattern, "%d", strconv.Itoa(i), 1), nil
}
snapshotExists := false
snapshots, err := inst.Snapshots()
if err != nil {
return "", err
}
for _, snap := range snapshots {
_, snapOnlyName, _ := api.GetParentAndSnapshotName(snap.Name())
if snapOnlyName == pattern {
snapshotExists = true
break
}
}
// Append '-0', '-1', etc. if the actual pattern/snapshot name already exists
if snapshotExists {
pattern = fmt.Sprintf("%s-%%d", pattern)
var i int
_ = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
i = tx.GetNextInstanceSnapshotIndex(ctx, inst.Project().Name, inst.Name(), pattern)
return nil
})
return strings.Replace(pattern, "%d", strconv.Itoa(i), 1), nil
}
return pattern, nil
}
// temporaryName returns the temporary instance name using a stable random generator.
// The returned string is a valid DNS name.
func temporaryName(instUUID string) (string, error) {
r, err := localUtil.GetStableRandomGenerator(instUUID)
if err != nil {
return "", err
}
// The longest temporary name is move-of-18446744073709551615 which has a length
// of 30 characters since 18446744073709551615 is the biggest value for an uint64.
// The prefix is attached to have a valid DNS name that doesn't start with numbers.
return fmt.Sprintf("move-of-%d", r.Uint64()), nil
}
// MoveTemporaryName returns a name derived from the instance's volatile.uuid, to use when moving an instance
// across pools or cluster members which can be used for the naming the temporary copy before deleting the original
// instance and renaming the copy to the original name.
// If volatile.uuid is not set, a new UUID is generated and stored in the instance's config.
func MoveTemporaryName(inst Instance) (string, error) {
instUUID := inst.LocalConfig()["volatile.uuid"]
if instUUID == "" {
instUUID = uuid.New().String()
err := inst.VolatileSet(map[string]string{"volatile.uuid": instUUID})
if err != nil {
return "", fmt.Errorf("Failed setting volatile.uuid to %s: %w", instUUID, err)
}
}
return temporaryName(instUUID)
}
// IsSameLogicalInstance returns true if the supplied Instance and db.Instance have the same project and name or
// if they have the same volatile.uuid values.
func IsSameLogicalInstance(inst Instance, dbInst *db.InstanceArgs) bool {
// Instance name is unique within a project.
if dbInst.Project == inst.Project().Name && dbInst.Name == inst.Name() {
return true
}
// Don't trigger duplicate resource errors for temporary copies.
if dbInst.Config["volatile.uuid"] == inst.LocalConfig()["volatile.uuid"] {
// Accommodate moving instances between storage pools.
// Check temporary copy against source.
tempName, err := temporaryName(inst.LocalConfig()["volatile.uuid"])
if err != nil {
return false
}
if dbInst.Name == tempName {
return true
}
// Check source against temporary copy.
tempName, err = temporaryName(dbInst.Config["volatile.uuid"])
if err != nil {
return false
}
if inst.Name() == tempName {
return true
}
// Accommodate moving instances between projects.
if dbInst.Project != inst.Project().Name {
return true
}
}
return false
}
// SnapshotToProtobuf converts a snapshot record to a migration snapshot record.
func SnapshotToProtobuf(snap *api.InstanceSnapshot) *migration.Snapshot {
config := make([]*migration.Config, 0, len(snap.Config))
for k, v := range snap.Config {
kCopy := string(k)
vCopy := string(v)
config = append(config, &migration.Config{Key: &kCopy, Value: &vCopy})
}
devices := make([]*migration.Device, 0, len(snap.Devices))
for name, d := range snap.Devices {
props := make([]*migration.Config, 0, len(snap.Devices))
for k, v := range d {
// Local loop vars.
kCopy := string(k)
vCopy := string(v)
props = append(props, &migration.Config{Key: &kCopy, Value: &vCopy})
}
nameCopy := name // Local loop var.
devices = append(devices, &migration.Device{Name: &nameCopy, Config: props})
}
isEphemeral := snap.Ephemeral
archID, _ := osarch.ArchitectureID(snap.Architecture)
arch := int32(archID)
stateful := snap.Stateful
creationDate := snap.CreatedAt.UTC().Unix()
lastUsedDate := snap.LastUsedAt.UTC().Unix()
expiryDate := snap.ExpiresAt.UTC().Unix()
return &migration.Snapshot{
Name: &snap.Name,
LocalConfig: config,
Profiles: snap.Profiles,
Ephemeral: &isEphemeral,
LocalDevices: devices,
Architecture: &arch,
Stateful: &stateful,
CreationDate: &creationDate,
LastUsedDate: &lastUsedDate,
ExpiryDate: &expiryDate,
}
}
// SnapshotProtobufToInstanceArgs converts a migration snapshot record to DB instance record format.
func SnapshotProtobufToInstanceArgs(s *state.State, inst Instance, snap *migration.Snapshot) (*db.InstanceArgs, error) {
snapConfig := snap.GetLocalConfig()
config := make(map[string]string, len(snapConfig))
for _, ent := range snapConfig {
config[ent.GetKey()] = ent.GetValue()
}
snapDevices := snap.GetLocalDevices()
devices := make(deviceConfig.Devices, len(snapDevices))
for _, ent := range snap.GetLocalDevices() {
entConfig := ent.GetConfig()
props := make(map[string]string, len(entConfig))
for _, prop := range entConfig {
props[prop.GetKey()] = prop.GetValue()
}
devices[ent.GetName()] = props
}
var profiles []api.Profile
err := s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
var err error
profiles, err = tx.GetProfiles(ctx, inst.Project().Name, snap.Profiles)
return err
})
if err != nil {
return nil, err
}
args := db.InstanceArgs{
Architecture: int(snap.GetArchitecture()),
Config: config,
Description: inst.Description(),
Type: inst.Type(),
Snapshot: true,
Devices: devices,
Ephemeral: snap.GetEphemeral(),
Name: inst.Name() + instance.SnapshotDelimiter + snap.GetName(),
Profiles: profiles,
Stateful: snap.GetStateful(),
Project: inst.Project().Name,
}
if snap.GetCreationDate() != 0 {
args.CreationDate = time.Unix(snap.GetCreationDate(), 0)
}
if snap.GetLastUsedDate() != 0 {
args.LastUsedDate = time.Unix(snap.GetLastUsedDate(), 0)
}
if snap.GetExpiryDate() != 0 {
args.ExpiryDate = time.Unix(snap.GetExpiryDate(), 0)
}
return &args, nil
}
// ResourceUsage returns an instance's expected CPU, memory and disk usage.
func ResourceUsage(instConfig map[string]string, instDevices map[string]map[string]string, instType api.InstanceType) (int64, int64, int64, error) {
var err error
limitsCPU := instConfig["limits.cpu"]
limitsMemory := instConfig["limits.memory"]
cpuUsage := int64(0)
memoryUsage := int64(0)
diskUsage := int64(0)
// Parse limits.cpu.
if limitsCPU != "" {
// Check if using shared CPU limits.
cpuUsage, err = strconv.ParseInt(limitsCPU, 10, 64)
if err != nil {
// Or get count of pinned CPUs.
pinnedCPUs, err := resources.ParseCpuset(limitsCPU)
if err != nil {
return -1, -1, -1, fmt.Errorf("Failed parsing instance resources limits.cpu: %w", err)
}
cpuUsage = int64(len(pinnedCPUs))
}
} else if instType == api.InstanceTypeVM {
// Apply VM CPU cores defaults if not specified.
cpuUsage = qemudefault.CPUCores
}
// Parse limits.memory.
memoryLimitStr := limitsMemory
// Apply VM memory limit defaults if not specified.
if instType == api.InstanceTypeVM && memoryLimitStr == "" {
memoryLimitStr = qemudefault.MemSize
}
if memoryLimitStr != "" {
memoryLimit, err := units.ParseByteSizeString(memoryLimitStr)
if err != nil {
return -1, -1, -1, fmt.Errorf("Failed parsing instance resources limits.memory: %w", err)
}
memoryUsage = int64(memoryLimit)
}
// Parse root disk size.
_, rootDiskConfig, err := instance.GetRootDiskDevice(instDevices)
if err == nil {
rootDiskSizeStr := rootDiskConfig["size"]
// Apply VM root disk size defaults if not specified.
if instType == api.InstanceTypeVM && rootDiskSizeStr == "" {
rootDiskSizeStr = storageDrivers.DefaultBlockSize
}
if rootDiskSizeStr != "" {
rootDiskSize, err := units.ParseByteSizeString(rootDiskSizeStr)
if err != nil {
return -1, -1, -1, fmt.Errorf("Failed parsing instance resources root disk size: %w", err)
}
diskUsage = int64(rootDiskSize)
}
}
return cpuUsage, memoryUsage, diskUsage, nil
}