mirror of
https://github.com/lxc/incus
synced 2026-08-02 05:26:46 +00:00
systemd credentials name could be abused to escape the credentials folder and allow for arbitrary writes to the host filesystem allowing for privilege escalation and denial of service attacks. We now use Go's OpenRoot (openat2) to restrict all file interactions to the "credentials" directory, avoiding such attacks. This addresses CVE-2026-33945 Reported-by: https://7asecurity.com Signed-off-by: Stéphane Graber <stgraber@stgraber.org>
9316 lines
247 KiB
Go
9316 lines
247 KiB
Go
package drivers
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"maps"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"runtime"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/checkpoint-restore/go-criu/v6/crit"
|
|
"github.com/flosch/pongo2/v6"
|
|
"github.com/google/uuid"
|
|
"github.com/gorilla/websocket"
|
|
"github.com/kballard/go-shellquote"
|
|
liblxc "github.com/lxc/go-lxc"
|
|
"github.com/pkg/sftp"
|
|
"golang.org/x/sync/errgroup"
|
|
"golang.org/x/sys/unix"
|
|
"google.golang.org/protobuf/proto"
|
|
yaml "gopkg.in/yaml.v2"
|
|
|
|
internalInstance "github.com/lxc/incus/v6/internal/instance"
|
|
"github.com/lxc/incus/v6/internal/instancewriter"
|
|
internalIO "github.com/lxc/incus/v6/internal/io"
|
|
"github.com/lxc/incus/v6/internal/jmap"
|
|
"github.com/lxc/incus/v6/internal/linux"
|
|
"github.com/lxc/incus/v6/internal/migration"
|
|
"github.com/lxc/incus/v6/internal/netutils"
|
|
"github.com/lxc/incus/v6/internal/rsync"
|
|
"github.com/lxc/incus/v6/internal/server/apparmor"
|
|
"github.com/lxc/incus/v6/internal/server/cgroup"
|
|
"github.com/lxc/incus/v6/internal/server/daemon"
|
|
"github.com/lxc/incus/v6/internal/server/db"
|
|
"github.com/lxc/incus/v6/internal/server/db/cluster"
|
|
"github.com/lxc/incus/v6/internal/server/db/operationtype"
|
|
"github.com/lxc/incus/v6/internal/server/device"
|
|
deviceConfig "github.com/lxc/incus/v6/internal/server/device/config"
|
|
"github.com/lxc/incus/v6/internal/server/device/nictype"
|
|
"github.com/lxc/incus/v6/internal/server/instance"
|
|
"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/ip"
|
|
"github.com/lxc/incus/v6/internal/server/lifecycle"
|
|
"github.com/lxc/incus/v6/internal/server/locking"
|
|
"github.com/lxc/incus/v6/internal/server/metrics"
|
|
localMigration "github.com/lxc/incus/v6/internal/server/migration"
|
|
"github.com/lxc/incus/v6/internal/server/network"
|
|
"github.com/lxc/incus/v6/internal/server/operations"
|
|
"github.com/lxc/incus/v6/internal/server/project"
|
|
"github.com/lxc/incus/v6/internal/server/response"
|
|
"github.com/lxc/incus/v6/internal/server/seccomp"
|
|
"github.com/lxc/incus/v6/internal/server/state"
|
|
storagePools "github.com/lxc/incus/v6/internal/server/storage"
|
|
storageDrivers "github.com/lxc/incus/v6/internal/server/storage/drivers"
|
|
localUtil "github.com/lxc/incus/v6/internal/server/util"
|
|
internalUtil "github.com/lxc/incus/v6/internal/util"
|
|
"github.com/lxc/incus/v6/shared/api"
|
|
"github.com/lxc/incus/v6/shared/idmap"
|
|
"github.com/lxc/incus/v6/shared/ioprogress"
|
|
"github.com/lxc/incus/v6/shared/logger"
|
|
"github.com/lxc/incus/v6/shared/osarch"
|
|
"github.com/lxc/incus/v6/shared/revert"
|
|
"github.com/lxc/incus/v6/shared/subprocess"
|
|
"github.com/lxc/incus/v6/shared/termios"
|
|
"github.com/lxc/incus/v6/shared/units"
|
|
"github.com/lxc/incus/v6/shared/util"
|
|
"github.com/lxc/incus/v6/shared/ws"
|
|
)
|
|
|
|
// Helper functions.
|
|
func lxcSetConfigItem(c *liblxc.Container, key string, value string) error {
|
|
if c == nil {
|
|
return errors.New("Uninitialized go-lxc struct")
|
|
}
|
|
|
|
if !liblxc.RuntimeLiblxcVersionAtLeast(liblxc.Version(), 2, 1, 0) {
|
|
switch key {
|
|
case "lxc.uts.name":
|
|
key = "lxc.utsname"
|
|
case "lxc.pty.max":
|
|
key = "lxc.pts"
|
|
case "lxc.tty.dir":
|
|
key = "lxc.devttydir"
|
|
case "lxc.tty.max":
|
|
key = "lxc.tty"
|
|
case "lxc.apparmor.profile":
|
|
key = "lxc.aa_profile"
|
|
case "lxc.apparmor.allow_incomplete":
|
|
key = "lxc.aa_allow_incomplete"
|
|
case "lxc.selinux.context":
|
|
key = "lxc.se_context"
|
|
case "lxc.mount.fstab":
|
|
key = "lxc.mount"
|
|
case "lxc.console.path":
|
|
key = "lxc.console"
|
|
case "lxc.seccomp.profile":
|
|
key = "lxc.seccomp"
|
|
case "lxc.signal.halt":
|
|
key = "lxc.haltsignal"
|
|
case "lxc.signal.reboot":
|
|
key = "lxc.rebootsignal"
|
|
case "lxc.signal.stop":
|
|
key = "lxc.stopsignal"
|
|
case "lxc.log.syslog":
|
|
key = "lxc.syslog"
|
|
case "lxc.log.level":
|
|
key = "lxc.loglevel"
|
|
case "lxc.log.file":
|
|
key = "lxc.logfile"
|
|
case "lxc.init.cmd":
|
|
key = "lxc.init_cmd"
|
|
case "lxc.init.uid":
|
|
key = "lxc.init_uid"
|
|
case "lxc.init.gid":
|
|
key = "lxc.init_gid"
|
|
case "lxc.idmap":
|
|
key = "lxc.id_map"
|
|
}
|
|
}
|
|
|
|
if strings.HasPrefix(key, "lxc.prlimit.") {
|
|
if !liblxc.RuntimeLiblxcVersionAtLeast(liblxc.Version(), 2, 1, 0) {
|
|
return errors.New(`Process limits require liblxc >= 2.1`)
|
|
}
|
|
}
|
|
|
|
err := c.SetConfigItem(key, value)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to set LXC config: %s=%s", key, value)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func lxcStatusCode(state liblxc.State) api.StatusCode {
|
|
return map[int]api.StatusCode{
|
|
1: api.Stopped,
|
|
2: api.Starting,
|
|
3: api.Running,
|
|
4: api.Stopping,
|
|
5: api.Aborting,
|
|
6: api.Freezing,
|
|
7: api.Frozen,
|
|
8: api.Thawed,
|
|
9: api.Error,
|
|
}[int(state)]
|
|
}
|
|
|
|
// lxcCreate creates the DB storage records and sets up instance devices.
|
|
// Returns a revert fail function that can be used to undo this function if a subsequent step fails.
|
|
func lxcCreate(s *state.State, args db.InstanceArgs, p api.Project, op *operations.Operation) (instance.Instance, revert.Hook, error) {
|
|
reverter := revert.New()
|
|
defer reverter.Fail()
|
|
|
|
// Create the container struct
|
|
d := &lxc{
|
|
common: common{
|
|
state: s,
|
|
op: op,
|
|
|
|
architecture: args.Architecture,
|
|
creationDate: args.CreationDate,
|
|
dbType: args.Type,
|
|
description: args.Description,
|
|
ephemeral: args.Ephemeral,
|
|
expiryDate: args.ExpiryDate,
|
|
id: args.ID,
|
|
lastUsedDate: args.LastUsedDate,
|
|
localConfig: args.Config,
|
|
localDevices: args.Devices,
|
|
logger: logger.AddContext(logger.Ctx{"instanceType": args.Type, "instance": args.Name, "project": args.Project}),
|
|
name: args.Name,
|
|
node: args.Node,
|
|
profiles: args.Profiles,
|
|
project: p,
|
|
isSnapshot: args.Snapshot,
|
|
stateful: args.Stateful,
|
|
},
|
|
}
|
|
|
|
// Cleanup the zero values
|
|
if d.expiryDate.IsZero() {
|
|
d.expiryDate = time.Time{}
|
|
}
|
|
|
|
if d.creationDate.IsZero() {
|
|
d.creationDate = time.Time{}
|
|
}
|
|
|
|
if d.lastUsedDate.IsZero() {
|
|
d.lastUsedDate = time.Time{}
|
|
}
|
|
|
|
if args.Snapshot {
|
|
d.logger.Info("Creating instance snapshot", logger.Ctx{"ephemeral": d.ephemeral})
|
|
} else {
|
|
d.logger.Info("Creating instance", logger.Ctx{"ephemeral": d.ephemeral})
|
|
}
|
|
|
|
// Load the config.
|
|
err := d.init()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("Failed to expand config: %w", err)
|
|
}
|
|
|
|
// When not a snapshot, perform full validation.
|
|
if !args.Snapshot {
|
|
// Validate expanded config (allows mixed instance types for profiles).
|
|
err = instance.ValidConfig(s.OS, d.expandedConfig, true, instancetype.Any)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("Invalid config: %w", err)
|
|
}
|
|
|
|
err = instance.ValidDevices(s, d.project, d.Type(), d.localDevices, d.expandedDevices)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("Invalid devices: %w", err)
|
|
}
|
|
}
|
|
|
|
_, rootDiskDevice, err := d.getRootDiskDevice()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("Failed getting root disk: %w", err)
|
|
}
|
|
|
|
if rootDiskDevice["pool"] == "" {
|
|
return nil, nil, errors.New("The instance's root device is missing the pool property")
|
|
}
|
|
|
|
// Initialize the storage pool.
|
|
d.storagePool, err = storagePools.LoadByName(d.state, rootDiskDevice["pool"])
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("Failed loading storage pool: %w", err)
|
|
}
|
|
|
|
volType, err := storagePools.InstanceTypeToVolumeType(d.Type())
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
storagePoolSupported := slices.Contains(d.storagePool.Driver().Info().VolumeTypes, volType)
|
|
|
|
if !storagePoolSupported {
|
|
return nil, nil, errors.New("Storage pool does not support instance type")
|
|
}
|
|
|
|
// Setup the initial idmap config.
|
|
var idmapSet *idmap.Set
|
|
base := int64(0)
|
|
if !d.IsPrivileged() {
|
|
idmapSet, base, err = d.findIdmap()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
idmapSetJSON, err := idmapSet.ToJSON()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("Failed to encode ID map: %w", err)
|
|
}
|
|
|
|
v := map[string]string{
|
|
"volatile.idmap.next": idmapSetJSON,
|
|
"volatile.idmap.base": fmt.Sprintf("%v", base),
|
|
}
|
|
|
|
// Invalidate the idmap cache.
|
|
d.idmapset = nil
|
|
|
|
// Set last_state if not currently set.
|
|
if d.localConfig["volatile.last_state.idmap"] == "" {
|
|
v["volatile.last_state.idmap"] = "[]"
|
|
}
|
|
|
|
err = d.VolatileSet(v)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Re-run init to update the idmap.
|
|
err = d.init()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if !d.IsSnapshot() {
|
|
// Add devices to container.
|
|
cleanup, err := d.devicesAdd(d, false)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
reverter.Add(cleanup)
|
|
}
|
|
|
|
if d.isSnapshot {
|
|
d.logger.Info("Created instance snapshot", logger.Ctx{"ephemeral": d.ephemeral})
|
|
} else {
|
|
d.logger.Info("Created instance", logger.Ctx{"ephemeral": d.ephemeral})
|
|
}
|
|
|
|
if d.isSnapshot {
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceSnapshotCreated.Event(d, nil))
|
|
} else {
|
|
// Add instance to authorizer.
|
|
err = d.state.Authorizer.AddInstance(d.state.ShutdownCtx, d.project.Name, d.Name())
|
|
if err != nil {
|
|
logger.Error("Failed to add instance to authorizer", logger.Ctx{"instanceName": d.Name(), "projectName": d.project.Name, "error": err})
|
|
}
|
|
|
|
reverter.Add(func() { _ = d.state.Authorizer.DeleteInstance(d.state.ShutdownCtx, d.project.Name, d.Name()) })
|
|
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceCreated.Event(d, map[string]any{
|
|
"type": api.InstanceTypeContainer,
|
|
"storage-pool": d.storagePool.Name(),
|
|
"location": d.Location(),
|
|
}))
|
|
}
|
|
|
|
cleanup := reverter.Clone().Fail
|
|
reverter.Success()
|
|
|
|
return d, cleanup, err
|
|
}
|
|
|
|
func lxcLoad(s *state.State, args db.InstanceArgs, p api.Project) (instance.Instance, error) {
|
|
// Create the container struct
|
|
d := lxcInstantiate(s, args, nil, p)
|
|
|
|
// Expand config and devices
|
|
err := d.(*lxc).expandConfig()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return d, nil
|
|
}
|
|
|
|
// Unload is called by the garbage collector.
|
|
func lxcUnload(d *lxc) {
|
|
d.release()
|
|
}
|
|
|
|
// release releases any internal reference to a liblxc container, invalidating the go-lxc cache.
|
|
func (d *lxc) release() {
|
|
d.cMu.Lock()
|
|
defer d.cMu.Unlock()
|
|
|
|
if d.c != nil {
|
|
_ = d.c.Release()
|
|
d.c = nil
|
|
}
|
|
}
|
|
|
|
// Create a container struct without initializing it.
|
|
func lxcInstantiate(s *state.State, args db.InstanceArgs, expandedDevices deviceConfig.Devices, p api.Project) instance.Instance {
|
|
d := &lxc{
|
|
common: common{
|
|
state: s,
|
|
|
|
architecture: args.Architecture,
|
|
creationDate: args.CreationDate,
|
|
dbType: args.Type,
|
|
description: args.Description,
|
|
ephemeral: args.Ephemeral,
|
|
expiryDate: args.ExpiryDate,
|
|
id: args.ID,
|
|
lastUsedDate: args.LastUsedDate,
|
|
localConfig: args.Config,
|
|
localDevices: args.Devices,
|
|
logger: logger.AddContext(logger.Ctx{"instanceType": args.Type, "instance": args.Name, "project": args.Project}),
|
|
name: args.Name,
|
|
node: args.Node,
|
|
profiles: args.Profiles,
|
|
project: p,
|
|
isSnapshot: args.Snapshot,
|
|
stateful: args.Stateful,
|
|
},
|
|
}
|
|
|
|
// Cleanup the zero values
|
|
if d.expiryDate.IsZero() {
|
|
d.expiryDate = time.Time{}
|
|
}
|
|
|
|
if d.creationDate.IsZero() {
|
|
d.creationDate = time.Time{}
|
|
}
|
|
|
|
if d.lastUsedDate.IsZero() {
|
|
d.lastUsedDate = time.Time{}
|
|
}
|
|
|
|
// This is passed during expanded config validation.
|
|
if expandedDevices != nil {
|
|
d.expandedDevices = expandedDevices
|
|
}
|
|
|
|
return d
|
|
}
|
|
|
|
// The LXC container driver.
|
|
type lxc struct {
|
|
common
|
|
|
|
// Config handling.
|
|
fromHook bool
|
|
|
|
cMu sync.Mutex
|
|
cFinalizer sync.Once
|
|
|
|
// Cached handles.
|
|
// Do not use these variables directly, instead use their associated get functions so they
|
|
// will be initialized on demand.
|
|
c *liblxc.Container // Use d.initLXC() instead of accessing this directly.
|
|
|
|
cConfig bool
|
|
idmapset *idmap.Set
|
|
}
|
|
|
|
var idmapLock sync.Mutex
|
|
|
|
func (d *lxc) findIdmap() (*idmap.Set, int64, error) {
|
|
if d.state.OS.IdmapSet == nil {
|
|
return nil, 0, errors.New("System doesn't have a functional idmap setup")
|
|
}
|
|
|
|
idmapSize := func(size string) (int64, error) {
|
|
var idMapSize int64
|
|
if size == "" || size == "auto" {
|
|
if util.IsTrue(d.expandedConfig["security.idmap.isolated"]) {
|
|
idMapSize = 65536
|
|
} else {
|
|
if len(d.state.OS.IdmapSet.Entries) != 2 {
|
|
return 0, fmt.Errorf("Bad initial idmap: %v", d.state.OS.IdmapSet)
|
|
}
|
|
|
|
idMapSize = d.state.OS.IdmapSet.Entries[0].MapRange
|
|
}
|
|
} else {
|
|
size, err := strconv.ParseInt(size, 10, 64)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
idMapSize = size
|
|
}
|
|
|
|
return idMapSize, nil
|
|
}
|
|
|
|
rawMaps, err := idmap.NewSetFromIncusIDMap(d.expandedConfig["raw.idmap"])
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
mkIdmap := func(offset int64, size int64) (*idmap.Set, error) {
|
|
set := &idmap.Set{Entries: []idmap.Entry{
|
|
{IsUID: true, NSID: 0, HostID: offset, MapRange: size},
|
|
{IsGID: true, NSID: 0, HostID: offset, MapRange: size},
|
|
}}
|
|
|
|
for _, ent := range rawMaps.Entries {
|
|
err := set.AddSafe(ent)
|
|
if err != nil && errors.Is(err, idmap.ErrHostIDIsSubID) {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return set, nil
|
|
}
|
|
|
|
if !util.IsTrue(d.expandedConfig["security.idmap.isolated"]) {
|
|
// Create a new set based from the global one.
|
|
newIdmapset := idmap.Set{Entries: make([]idmap.Entry, len(d.state.OS.IdmapSet.Entries))}
|
|
copy(newIdmapset.Entries, d.state.OS.IdmapSet.Entries)
|
|
|
|
// Restrict the range sizes if specified.
|
|
if d.expandedConfig["security.idmap.size"] != "" {
|
|
size, err := idmapSize(d.expandedConfig["security.idmap.size"])
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
for k, ent := range newIdmapset.Entries {
|
|
if ent.MapRange < size {
|
|
continue
|
|
}
|
|
|
|
newIdmapset.Entries[k].MapRange = size
|
|
}
|
|
}
|
|
|
|
// Apply the raw idmap entries.
|
|
for _, ent := range rawMaps.Entries {
|
|
err := newIdmapset.AddSafe(ent)
|
|
if err != nil && errors.Is(err, idmap.ErrHostIDIsSubID) {
|
|
return nil, 0, err
|
|
}
|
|
}
|
|
|
|
return &newIdmapset, 0, nil
|
|
}
|
|
|
|
size, err := idmapSize(d.expandedConfig["security.idmap.size"])
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
if d.expandedConfig["security.idmap.base"] != "" {
|
|
offset, err := strconv.ParseInt(d.expandedConfig["security.idmap.base"], 10, 64)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
set, err := mkIdmap(offset, size)
|
|
if err != nil && errors.Is(err, idmap.ErrHostIDIsSubID) {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return set, offset, nil
|
|
}
|
|
|
|
idmapLock.Lock()
|
|
defer idmapLock.Unlock()
|
|
|
|
cts, err := instance.LoadNodeAll(d.state, instancetype.Container)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
offset := d.state.OS.IdmapSet.Entries[0].HostID + 65536
|
|
|
|
mapentries := idmap.ByHostID{}
|
|
for _, container := range cts {
|
|
if container.Type() != instancetype.Container {
|
|
continue
|
|
}
|
|
|
|
/* Don't change our map Just Because. */
|
|
if container.ID() == d.id {
|
|
continue
|
|
}
|
|
|
|
if container.IsPrivileged() {
|
|
continue
|
|
}
|
|
|
|
if util.IsFalseOrEmpty(container.ExpandedConfig()["security.idmap.isolated"]) {
|
|
continue
|
|
}
|
|
|
|
if container.ExpandedConfig()["volatile.idmap.base"] == "" {
|
|
continue
|
|
}
|
|
|
|
cBase, err := strconv.ParseInt(container.ExpandedConfig()["volatile.idmap.base"], 10, 64)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
cSize, err := idmapSize(container.ExpandedConfig()["security.idmap.size"])
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
mapentries.Entries = append(mapentries.Entries, idmap.Entry{HostID: int64(cBase), MapRange: cSize})
|
|
}
|
|
|
|
sort.Sort(mapentries)
|
|
|
|
for i := range mapentries.Entries {
|
|
if i == 0 {
|
|
if mapentries.Entries[0].HostID < offset+size {
|
|
offset = mapentries.Entries[0].HostID + mapentries.Entries[0].MapRange
|
|
continue
|
|
}
|
|
|
|
set, err := mkIdmap(offset, size)
|
|
if err != nil && errors.Is(err, idmap.ErrHostIDIsSubID) {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return set, offset, nil
|
|
}
|
|
|
|
if mapentries.Entries[i-1].HostID+mapentries.Entries[i-1].MapRange > offset {
|
|
offset = mapentries.Entries[i-1].HostID + mapentries.Entries[i-1].MapRange
|
|
continue
|
|
}
|
|
|
|
offset = mapentries.Entries[i-1].HostID + mapentries.Entries[i-1].MapRange
|
|
if offset+size < mapentries.Entries[i].HostID {
|
|
set, err := mkIdmap(offset, size)
|
|
if err != nil && errors.Is(err, idmap.ErrHostIDIsSubID) {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return set, offset, nil
|
|
}
|
|
|
|
offset = mapentries.Entries[i].HostID + mapentries.Entries[i].MapRange
|
|
}
|
|
|
|
if offset+size <= d.state.OS.IdmapSet.Entries[0].HostID+d.state.OS.IdmapSet.Entries[0].MapRange {
|
|
set, err := mkIdmap(offset, size)
|
|
if err != nil && errors.Is(err, idmap.ErrHostIDIsSubID) {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return set, offset, nil
|
|
}
|
|
|
|
return nil, 0, errors.New("Not enough uid/gid available for the container")
|
|
}
|
|
|
|
func (d *lxc) init() error {
|
|
// Compute the expanded config and device list
|
|
err := d.expandConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *lxc) initLXC(config bool) (*liblxc.Container, error) {
|
|
d.cMu.Lock()
|
|
defer d.cMu.Unlock()
|
|
|
|
// No need to go through all that for snapshots
|
|
if d.IsSnapshot() {
|
|
return nil, nil
|
|
}
|
|
|
|
// Check if being called from a hook
|
|
if d.fromHook {
|
|
return nil, errors.New("You can't use go-lxc from inside a LXC hook")
|
|
}
|
|
|
|
// Check if already initialized
|
|
if d.c != nil && (!config || d.cConfig) {
|
|
return d.c, nil
|
|
}
|
|
|
|
// As we are now going to be initialising a liblxc.Container reference, set the finalizer so that it is
|
|
// cleaned up (if needed) when the garbage collector destroys this instance struct.
|
|
d.cFinalizer.Do(func() { runtime.SetFinalizer(d, lxcUnload) })
|
|
|
|
reverter := revert.New()
|
|
defer reverter.Fail()
|
|
|
|
// Load the go-lxc struct
|
|
cname := project.Instance(d.Project().Name, d.Name())
|
|
cc, err := liblxc.NewContainer(cname, d.state.OS.LxcPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
reverter.Add(func() {
|
|
_ = cc.Release()
|
|
})
|
|
|
|
// Load cgroup abstraction
|
|
cg, err := d.cgroup(cc, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Setup logging
|
|
logfile := d.LogFilePath()
|
|
err = lxcSetConfigItem(cc, "lxc.log.file", logfile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
logLevel := "warn"
|
|
if daemon.Debug {
|
|
logLevel = "trace"
|
|
} else if daemon.Verbose {
|
|
logLevel = "info"
|
|
}
|
|
|
|
err = lxcSetConfigItem(cc, "lxc.log.level", logLevel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if liblxc.RuntimeLiblxcVersionAtLeast(liblxc.Version(), 3, 0, 0) {
|
|
// Default size log buffer
|
|
err = lxcSetConfigItem(cc, "lxc.console.buffer.size", "auto")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = lxcSetConfigItem(cc, "lxc.console.size", "auto")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// File to dump ringbuffer contents to when requested or
|
|
// container shutdown.
|
|
consoleBufferLogFile := d.ConsoleBufferLogPath()
|
|
err = lxcSetConfigItem(cc, "lxc.console.logfile", consoleBufferLogFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if d.state.OS.ContainerCoreScheduling {
|
|
err = lxcSetConfigItem(cc, "lxc.sched.core", "1")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else if d.state.OS.CoreScheduling {
|
|
err = lxcSetConfigItem(cc, "lxc.hook.start-host", fmt.Sprintf("/proc/%d/exe forkcoresched 1", os.Getpid()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Allow for lightweight init
|
|
d.cConfig = config
|
|
if !config {
|
|
if d.c != nil {
|
|
_ = d.c.Release()
|
|
}
|
|
|
|
d.c = cc
|
|
|
|
reverter.Success()
|
|
return cc, err
|
|
}
|
|
|
|
if d.IsPrivileged() {
|
|
// Base config
|
|
toDrop := "sys_time sys_module sys_rawio"
|
|
if !d.state.OS.AppArmorStacking || d.state.OS.AppArmorStacked {
|
|
toDrop = toDrop + " mac_admin mac_override"
|
|
}
|
|
|
|
err = lxcSetConfigItem(cc, "lxc.cap.drop", toDrop)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Set an appropriate /proc, /sys/ and /sys/fs/cgroup
|
|
mounts := []string{}
|
|
if d.IsPrivileged() && !d.state.OS.RunningInUserNS {
|
|
mounts = append(mounts, "proc:mixed")
|
|
mounts = append(mounts, "sys:mixed")
|
|
} else {
|
|
mounts = append(mounts, "proc:rw")
|
|
mounts = append(mounts, "sys:rw")
|
|
}
|
|
|
|
cgInfo := cgroup.GetInfo()
|
|
if cgInfo.Namespacing {
|
|
if cgInfo.Layout == cgroup.CgroupsUnified {
|
|
mounts = append(mounts, "cgroup:rw:force")
|
|
} else {
|
|
mounts = append(mounts, "cgroup:mixed")
|
|
}
|
|
} else {
|
|
mounts = append(mounts, "cgroup:mixed")
|
|
}
|
|
|
|
err = lxcSetConfigItem(cc, "lxc.mount.auto", strings.Join(mounts, " "))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = lxcSetConfigItem(cc, "lxc.autodev", "1")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = lxcSetConfigItem(cc, "lxc.pty.max", "1024")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bindMounts := []string{
|
|
"/dev/fuse",
|
|
"/dev/net/tun",
|
|
"/sys/firmware/efi/efivars",
|
|
"/sys/fs/fuse/connections",
|
|
"/sys/fs/pstore",
|
|
"/sys/kernel/config",
|
|
"/sys/kernel/debug",
|
|
"/sys/kernel/security",
|
|
"/sys/kernel/tracing",
|
|
}
|
|
|
|
// Handle unprivileged binfmt_misc.
|
|
if d.IsPrivileged() || !d.state.OS.UnprivBinfmt {
|
|
bindMounts = append(bindMounts, "/proc/sys/fs/binfmt_misc")
|
|
}
|
|
|
|
// Pass in /dev/zfs to the container if delegation is supported on the system.
|
|
// This is only done for unprivileged containers as delegation is tied to the user namespace.
|
|
if !d.IsPrivileged() && storageDrivers.ZFSSupportsDelegation() && util.PathExists("/dev/zfs") {
|
|
bindMounts = append(bindMounts, "/dev/zfs")
|
|
}
|
|
|
|
if d.IsPrivileged() && !d.state.OS.RunningInUserNS {
|
|
err = lxcSetConfigItem(cc, "lxc.mount.entry", "mqueue dev/mqueue mqueue rw,relatime,create=dir,optional 0 0")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
bindMounts = append(bindMounts, "/dev/mqueue")
|
|
}
|
|
|
|
for _, mnt := range bindMounts {
|
|
if !util.PathExists(mnt) {
|
|
continue
|
|
}
|
|
|
|
if internalUtil.IsDir(mnt) {
|
|
err = lxcSetConfigItem(cc, "lxc.mount.entry", fmt.Sprintf("%s %s none rbind,create=dir,optional 0 0", mnt, strings.TrimPrefix(mnt, "/")))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
err = lxcSetConfigItem(cc, "lxc.mount.entry", fmt.Sprintf("%s %s none bind,create=file,optional 0 0", mnt, strings.TrimPrefix(mnt, "/")))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// For lxcfs
|
|
templateConfDir := os.Getenv("INCUS_LXC_TEMPLATE_CONFIG")
|
|
if templateConfDir == "" {
|
|
templateConfDir = "/usr/share/lxc/config"
|
|
}
|
|
|
|
if util.PathExists(fmt.Sprintf("%s/common.conf.d/", templateConfDir)) {
|
|
err = lxcSetConfigItem(cc, "lxc.include", fmt.Sprintf("%s/common.conf.d/", templateConfDir))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Configure devices cgroup
|
|
if d.IsPrivileged() && !d.state.OS.RunningInUserNS && d.state.OS.CGInfo.Supports(cgroup.Devices, cg) {
|
|
if d.state.OS.CGInfo.Layout == cgroup.CgroupsUnified {
|
|
err = lxcSetConfigItem(cc, "lxc.cgroup2.devices.deny", "a")
|
|
} else {
|
|
err = lxcSetConfigItem(cc, "lxc.cgroup.devices.deny", "a")
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
devices := []string{
|
|
"b *:* m", // Allow mknod of block devices
|
|
"c *:* m", // Allow mknod of char devices
|
|
"c 136:* rwm", // /dev/pts devices
|
|
"c 1:3 rwm", // /dev/null
|
|
"c 1:5 rwm", // /dev/zero
|
|
"c 1:7 rwm", // /dev/full
|
|
"c 1:8 rwm", // /dev/random
|
|
"c 1:9 rwm", // /dev/urandom
|
|
"c 5:0 rwm", // /dev/tty
|
|
"c 5:1 rwm", // /dev/console
|
|
"c 5:2 rwm", // /dev/ptmx
|
|
"c 10:229 rwm", // /dev/fuse
|
|
"c 10:200 rwm", // /dev/net/tun
|
|
}
|
|
|
|
if storageDrivers.ZFSSupportsDelegation() {
|
|
devices = append(devices, "c 10:249 rwm")
|
|
}
|
|
|
|
for _, dev := range devices {
|
|
if d.state.OS.CGInfo.Layout == cgroup.CgroupsUnified {
|
|
err = lxcSetConfigItem(cc, "lxc.cgroup2.devices.allow", dev)
|
|
} else {
|
|
err = lxcSetConfigItem(cc, "lxc.cgroup.devices.allow", dev)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
if d.IsNesting() {
|
|
/*
|
|
* mount extra /proc and /sys to work around kernel
|
|
* restrictions on remounting them when covered
|
|
*/
|
|
err = lxcSetConfigItem(cc, "lxc.mount.entry", "proc dev/.lxc/proc proc create=dir,optional 0 0")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = lxcSetConfigItem(cc, "lxc.mount.entry", "sys dev/.lxc/sys sysfs create=dir,optional 0 0")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Setup architecture
|
|
personality, err := osarch.ArchitecturePersonality(d.architecture)
|
|
if err != nil {
|
|
personality, err = osarch.ArchitecturePersonality(d.state.OS.Architectures[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
err = lxcSetConfigItem(cc, "lxc.arch", personality)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Setup the hooks
|
|
err = lxcSetConfigItem(cc, "lxc.hook.version", "1")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Call the onstart hook on start.
|
|
err = lxcSetConfigItem(cc, "lxc.hook.pre-start", fmt.Sprintf("/proc/%d/exe callhook %s %s %s start", os.Getpid(), internalUtil.VarPath(""), util.SingleQuote(d.Project().Name), util.SingleQuote(d.Name())))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Call the onstopns hook on stop but before namespaces are unmounted.
|
|
err = lxcSetConfigItem(cc, "lxc.hook.stop", fmt.Sprintf("%s callhook %s %s %s stopns", d.state.OS.ExecPath, internalUtil.VarPath(""), util.SingleQuote(d.Project().Name), util.SingleQuote(d.Name())))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Call the onstop hook on stop.
|
|
err = lxcSetConfigItem(cc, "lxc.hook.post-stop", fmt.Sprintf("%s callhook %s %s %s stop", d.state.OS.ExecPath, internalUtil.VarPath(""), util.SingleQuote(d.Project().Name), util.SingleQuote(d.Name())))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Setup the console
|
|
err = lxcSetConfigItem(cc, "lxc.tty.max", "0")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Setup the hostname
|
|
err = lxcSetConfigItem(cc, "lxc.uts.name", d.Name())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Setup devIncus
|
|
if util.IsTrueOrEmpty(d.expandedConfig["security.guestapi"]) {
|
|
err = lxcSetConfigItem(cc, "lxc.mount.entry", fmt.Sprintf("%s dev/incus none bind,create=dir 0 0", internalUtil.VarPath("guestapi")))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Setup AppArmor
|
|
if d.state.OS.AppArmorAvailable {
|
|
if d.state.OS.AppArmorConfined || !d.state.OS.AppArmorAdmin {
|
|
// If confined but otherwise able to use AppArmor, use our own profile
|
|
curProfile := localUtil.AppArmorProfile()
|
|
curProfile = strings.TrimSuffix(curProfile, " (enforce)")
|
|
err := lxcSetConfigItem(cc, "lxc.apparmor.profile", curProfile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
// If not currently confined, use the container's profile
|
|
profile := apparmor.InstanceProfileName(d)
|
|
|
|
/* In the nesting case, we want to enable the inside
|
|
* daemon to load its profile. Unprivileged containers can
|
|
* load profiles, but privileged containers cannot, so
|
|
* let's not use a namespace so they can fall back to
|
|
* the old way of nesting, i.e. using the parent's
|
|
* profile.
|
|
*/
|
|
if d.state.OS.AppArmorStacking && !d.state.OS.AppArmorStacked {
|
|
profile = fmt.Sprintf("%s//&:%s:", profile, apparmor.InstanceNamespaceName(d))
|
|
}
|
|
|
|
err := lxcSetConfigItem(cc, "lxc.apparmor.profile", profile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
} else {
|
|
// Make sure that LXC won't try to apply an apparmor profile.
|
|
// This may fail on liblxc compiled without apparmor, so ignore errors.
|
|
_ = lxcSetConfigItem(cc, "lxc.apparmor.profile", "unconfined")
|
|
}
|
|
|
|
// Setup SELinux.
|
|
if d.state.OS.SELinuxAvailable && d.state.OS.SELinuxContextInstanceLXC != "" {
|
|
seContext, err := d.selinuxContext(d.state.OS.SELinuxContextInstanceLXC)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = lxcSetConfigItem(cc, "lxc.selinux.context", seContext)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Setup Seccomp if necessary
|
|
if seccomp.InstanceNeedsPolicy(d) {
|
|
err = lxcSetConfigItem(cc, "lxc.seccomp.profile", seccomp.ProfilePath(d))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Setup notification socket
|
|
// System requirement errors are handled during policy generation instead of here
|
|
ok, err := seccomp.InstanceNeedsIntercept(d.state, d)
|
|
if err == nil && ok {
|
|
err = lxcSetConfigItem(cc, "lxc.seccomp.notify.proxy", fmt.Sprintf("unix:%s", internalUtil.RunPath("seccomp.socket")))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Setup idmap
|
|
idmapset, err := d.NextIdmap()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if idmapset != nil {
|
|
lines := idmapset.ToLXCString()
|
|
for _, line := range lines {
|
|
err := lxcSetConfigItem(cc, "lxc.idmap", line)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Setup environment
|
|
for k, v := range d.expandedConfig {
|
|
// gendoc:generate(entity=instance, group=miscellaneous, key=environment.*)
|
|
// The specified key/value environment variables are exported to the instance and set for `incus exec`.
|
|
|
|
// ---
|
|
// type: string
|
|
// liveupdate: yes (exec)
|
|
// shortdesc: Environment variables to export
|
|
after, ok := strings.CutPrefix(k, "environment.")
|
|
if ok {
|
|
err = lxcSetConfigItem(cc, "lxc.environment", fmt.Sprintf("%s=%s", after, v))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
err = lxcSetConfigItem(cc, "lxc.environment", "CREDENTIALS_DIRECTORY=/dev/.incus-systemd-credentials")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = lxcSetConfigItem(cc, "lxc.mount.entry", fmt.Sprintf("%s dev/.incus-systemd-credentials none bind,ro,create=dir 0 0", filepath.Join(d.Path(), "credentials")))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Setup NVIDIA runtime
|
|
if util.IsTrue(d.expandedConfig["nvidia.runtime"]) {
|
|
hookDir := os.Getenv("INCUS_LXC_HOOK")
|
|
if hookDir == "" {
|
|
hookDir = "/usr/share/lxc/hooks"
|
|
}
|
|
|
|
hookPath := filepath.Join(hookDir, "nvidia")
|
|
if !util.PathExists(hookPath) {
|
|
return nil, errors.New("The NVIDIA LXC hook couldn't be found")
|
|
}
|
|
|
|
_, err := exec.LookPath("nvidia-container-cli")
|
|
if err != nil {
|
|
return nil, errors.New("The NVIDIA container tools couldn't be found")
|
|
}
|
|
|
|
err = lxcSetConfigItem(cc, "lxc.environment", "NVIDIA_VISIBLE_DEVICES=none")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nvidiaDriver := d.expandedConfig["nvidia.driver.capabilities"]
|
|
if nvidiaDriver == "" {
|
|
err = lxcSetConfigItem(cc, "lxc.environment", "NVIDIA_DRIVER_CAPABILITIES=compute,utility")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
err = lxcSetConfigItem(cc, "lxc.environment", fmt.Sprintf("NVIDIA_DRIVER_CAPABILITIES=%s", nvidiaDriver))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
nvidiaRequireCuda := d.expandedConfig["nvidia.require.cuda"]
|
|
if nvidiaRequireCuda == "" {
|
|
err = lxcSetConfigItem(cc, "lxc.environment", fmt.Sprintf("NVIDIA_REQUIRE_CUDA=%s", nvidiaRequireCuda))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
nvidiaRequireDriver := d.expandedConfig["nvidia.require.driver"]
|
|
if nvidiaRequireDriver == "" {
|
|
err = lxcSetConfigItem(cc, "lxc.environment", fmt.Sprintf("NVIDIA_REQUIRE_DRIVER=%s", nvidiaRequireDriver))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
err = lxcSetConfigItem(cc, "lxc.hook.mount", hookPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Memory limits
|
|
if d.state.OS.CGInfo.Supports(cgroup.Memory, cg) {
|
|
memory := d.expandedConfig["limits.memory"]
|
|
memoryEnforce := d.expandedConfig["limits.memory.enforce"]
|
|
memorySwap := d.expandedConfig["limits.memory.swap"]
|
|
memorySwapPriority := d.expandedConfig["limits.memory.swap.priority"]
|
|
|
|
// Configure the memory limits
|
|
if memory != "" {
|
|
valueInt, err := ParseMemoryStr(memory)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if memoryEnforce == "soft" {
|
|
err = cg.SetMemorySoftLimit(valueInt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
err = cg.SetMemoryLimit(valueInt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if d.state.OS.CGInfo.Supports(cgroup.MemorySwap, cg) {
|
|
if util.IsTrueOrEmpty(memorySwap) || util.IsFalse(memorySwap) {
|
|
err = cg.SetMemorySwapLimit(0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
// Additional memory as swap.
|
|
swapInt, err := units.ParseByteSizeString(memorySwap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = cg.SetMemorySwapLimit(swapInt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// If on CGroup1, set soft limit to value 10% less than hard limit.
|
|
if slices.Contains([]cgroup.Layout{cgroup.CgroupsLegacy, cgroup.CgroupsHybrid}, d.state.OS.CGInfo.Layout) {
|
|
err = cg.SetMemorySoftLimit(int64(float64(valueInt) * 0.9))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if d.state.OS.CGInfo.Supports(cgroup.MemorySwappiness, cg) {
|
|
// Configure the swappiness
|
|
if util.IsFalse(memorySwap) {
|
|
err = cg.SetMemorySwappiness(0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else if memorySwapPriority != "" {
|
|
priority, err := strconv.Atoi(memorySwapPriority)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Maximum priority (10) should be default swappiness (60).
|
|
err = cg.SetMemorySwappiness(int64(70 - priority))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// CPU limits
|
|
cpuPriority := d.expandedConfig["limits.cpu.priority"]
|
|
cpuAllowance := d.expandedConfig["limits.cpu.allowance"]
|
|
|
|
if (cpuPriority != "" || cpuAllowance != "") && d.state.OS.CGInfo.Supports(cgroup.CPU, cg) {
|
|
cpuShares, cpuCfsQuota, cpuCfsPeriod, err := cgroup.ParseCPU(cpuAllowance, cpuPriority)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if cpuShares != 1024 {
|
|
err = cg.SetCPUShare(cpuShares)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if cpuCfsPeriod != -1 && cpuCfsQuota != -1 {
|
|
err = cg.SetCPUCfsLimit(cpuCfsPeriod, cpuCfsQuota)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Disk priority limits.
|
|
diskPriority := d.ExpandedConfig()["limits.disk.priority"]
|
|
if diskPriority != "" {
|
|
if d.state.OS.CGInfo.Supports(cgroup.BlkioWeight, nil) {
|
|
priorityInt, err := strconv.Atoi(diskPriority)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
priority := priorityInt * 100
|
|
|
|
// Minimum valid value is 10
|
|
if priority == 0 {
|
|
priority = 10
|
|
}
|
|
|
|
err = cg.SetBlkioWeight(int64(priority))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
return nil, errors.New("Cannot apply limits.disk.priority as blkio.weight cgroup controller is missing")
|
|
}
|
|
}
|
|
|
|
// Processes
|
|
if d.state.OS.CGInfo.Supports(cgroup.Pids, cg) {
|
|
processes := d.expandedConfig["limits.processes"]
|
|
if processes != "" {
|
|
valueInt, err := strconv.ParseInt(processes, 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = cg.SetMaxProcesses(valueInt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Hugepages
|
|
if d.state.OS.CGInfo.Supports(cgroup.Hugetlb, cg) {
|
|
for i, key := range internalInstance.HugePageSizeKeys {
|
|
value := d.expandedConfig[key]
|
|
if value != "" {
|
|
value, err := units.ParseByteSizeString(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = cg.SetHugepagesLimit(internalInstance.HugePageSizeSuffix[i], value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Setup process limits
|
|
for k, v := range d.expandedConfig {
|
|
after, ok := strings.CutPrefix(k, "limits.kernel.")
|
|
if ok {
|
|
prlimitSuffix := after
|
|
prlimitKey := fmt.Sprintf("lxc.prlimit.%s", prlimitSuffix)
|
|
err = lxcSetConfigItem(cc, prlimitKey, v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Setup sysctls
|
|
for k, v := range d.expandedConfig {
|
|
// gendoc:generate(entity=instance, group=miscellaneous, key=linux.sysctl.*)
|
|
//
|
|
// ---
|
|
// type: string
|
|
// liveupdate: no
|
|
// condition: container
|
|
// shortdesc: Override for the corresponding `sysctl` setting in the container
|
|
after, ok := strings.CutPrefix(k, "linux.sysctl.")
|
|
if ok {
|
|
sysctlSuffix := after
|
|
sysctlKey := fmt.Sprintf("lxc.sysctl.%s", sysctlSuffix)
|
|
err = lxcSetConfigItem(cc, sysctlKey, v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Setup shmounts
|
|
if d.state.OS.LXCFeatures["mount_injection_file"] {
|
|
err = lxcSetConfigItem(cc, "lxc.mount.auto", fmt.Sprintf("shmounts:%s:/dev/.incus-mounts", d.ShmountsPath()))
|
|
} else {
|
|
err = lxcSetConfigItem(cc, "lxc.mount.entry", fmt.Sprintf("%s dev/.incus-mounts none bind,create=dir 0 0", d.ShmountsPath()))
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if d.c != nil {
|
|
_ = d.c.Release()
|
|
}
|
|
|
|
d.c = cc
|
|
reverter.Success()
|
|
|
|
return cc, err
|
|
}
|
|
|
|
var (
|
|
idmappedStorageMap map[unix.Fsid]idmap.StorageType = map[unix.Fsid]idmap.StorageType{}
|
|
idmappedStorageMapString map[string]idmap.StorageType = map[string]idmap.StorageType{}
|
|
idmappedStorageMapLock sync.Mutex
|
|
)
|
|
|
|
// IdmappedStorage determines if the container can use idmapped mounts.
|
|
func (d *lxc) IdmappedStorage(fspath string, fstype string) idmap.StorageType {
|
|
var mode idmap.StorageType = idmap.StorageTypeNone
|
|
var bindMount bool = fstype == "none" || fstype == ""
|
|
|
|
if !d.state.OS.LXCFeatures["idmapped_mounts_v2"] || !d.state.OS.IdmappedMounts {
|
|
return mode
|
|
}
|
|
|
|
buf := &unix.Statfs_t{}
|
|
|
|
if bindMount {
|
|
err := unix.Statfs(fspath, buf)
|
|
if err != nil {
|
|
d.logger.Error("Failed to statfs", logger.Ctx{"path": fspath, "err": err})
|
|
return mode
|
|
}
|
|
}
|
|
|
|
idmappedStorageMapLock.Lock()
|
|
defer idmappedStorageMapLock.Unlock()
|
|
|
|
if bindMount {
|
|
val, ok := idmappedStorageMap[buf.Fsid]
|
|
if ok {
|
|
// Return recorded idmapping type.
|
|
return val
|
|
}
|
|
} else {
|
|
val, ok := idmappedStorageMapString[fstype]
|
|
if ok {
|
|
// Return recorded idmapping type.
|
|
return val
|
|
}
|
|
}
|
|
|
|
if idmap.CanIdmapMount(fspath, fstype) {
|
|
// Use idmapped mounts.
|
|
mode = idmap.StorageTypeIdmapped
|
|
}
|
|
|
|
if bindMount {
|
|
idmappedStorageMap[buf.Fsid] = mode
|
|
} else {
|
|
idmappedStorageMapString[fstype] = mode
|
|
}
|
|
|
|
return mode
|
|
}
|
|
|
|
func (d *lxc) devIncusEventSend(eventType string, eventMessage map[string]any) error {
|
|
event := jmap.Map{}
|
|
event["type"] = eventType
|
|
event["timestamp"] = time.Now()
|
|
event["metadata"] = eventMessage
|
|
|
|
return d.state.DevIncusEvents.Send(d.ID(), eventType, eventMessage)
|
|
}
|
|
|
|
// RegisterDevices calls the Register() function on all of the instance's devices.
|
|
func (d *lxc) RegisterDevices() {
|
|
d.devicesRegister(d)
|
|
}
|
|
|
|
// deviceStart loads a new device and calls its Start() function.
|
|
func (d *lxc) deviceStart(dev device.Device, instanceRunning bool) (*deviceConfig.RunConfig, error) {
|
|
configCopy := dev.Config()
|
|
l := d.logger.AddContext(logger.Ctx{"device": dev.Name(), "type": configCopy["type"]})
|
|
l.Debug("Starting device")
|
|
|
|
reverter := revert.New()
|
|
defer reverter.Fail()
|
|
|
|
if instanceRunning && !dev.CanHotPlug() {
|
|
return nil, errors.New("Device cannot be started when instance is running")
|
|
}
|
|
|
|
runConf, err := dev.Start()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
reverter.Add(func() {
|
|
runConf, _ := dev.Stop()
|
|
if runConf != nil {
|
|
_ = d.runHooks(runConf.PostHooks)
|
|
}
|
|
})
|
|
|
|
// If runConf supplied, perform any container specific setup of device.
|
|
if runConf != nil {
|
|
// Shift device file ownership if needed before mounting into container.
|
|
// This needs to be done whether or not container is running.
|
|
if len(runConf.Mounts) > 0 {
|
|
err := d.deviceStaticShiftMounts(runConf.Mounts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// If container is running and then live attach device.
|
|
if instanceRunning {
|
|
// Attach mounts if requested.
|
|
if len(runConf.Mounts) > 0 {
|
|
err = d.deviceHandleMounts(runConf.Mounts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Add cgroup rules if requested.
|
|
if len(runConf.CGroups) > 0 {
|
|
err = d.deviceAddCgroupRules(runConf.CGroups)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Attach network interface if requested.
|
|
if len(runConf.NetworkInterface) > 0 {
|
|
err = d.deviceAttachNIC(dev.Name(), configCopy, runConf.NetworkInterface)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// If running, run post start hooks now (if not running, they will be run
|
|
// once the instance is started).
|
|
err = d.runHooks(runConf.PostHooks)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
reverter.Success()
|
|
|
|
return runConf, nil
|
|
}
|
|
|
|
// deviceStaticShiftMounts statically shift device mount files ownership to active idmap if needed.
|
|
func (d *lxc) deviceStaticShiftMounts(mounts []deviceConfig.MountEntryItem) error {
|
|
idmapSet, err := d.CurrentIdmap()
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to get idmap for device: %s", err)
|
|
}
|
|
|
|
// If there is an idmap being applied and the daemon is not running in a user namespace then shift the
|
|
// device files before they are mounted.
|
|
if idmapSet != nil && !d.state.OS.RunningInUserNS {
|
|
for _, mount := range mounts {
|
|
// Skip UID/GID shifting if OwnerShift mode is not static, or the host-side
|
|
// DevPath is empty (meaning an unmount request that doesn't need shifting).
|
|
if mount.OwnerShift != deviceConfig.MountOwnerShiftStatic || mount.DevPath == "" {
|
|
continue
|
|
}
|
|
|
|
err := idmapSet.ShiftPath(mount.DevPath, nil)
|
|
if err != nil {
|
|
// uidshift failing is weird, but not a big problem. Log and proceed.
|
|
d.logger.Debug("Failed to uidshift device", logger.Ctx{"mountDevPath": mount.DevPath, "err": err})
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// deviceAddCgroupRules live adds cgroup rules to a container.
|
|
func (d *lxc) deviceAddCgroupRules(cgroups []deviceConfig.RunConfigItem) error {
|
|
cc, err := d.initLXC(false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cg, err := d.cgroup(cc, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, rule := range cgroups {
|
|
// Only apply devices cgroup rules if container is running privileged and host has devices cgroup controller.
|
|
if strings.HasPrefix(rule.Key, "devices.") && (!d.isCurrentlyPrivileged() || d.state.OS.RunningInUserNS || !d.state.OS.CGInfo.Supports(cgroup.Devices, cg)) {
|
|
continue
|
|
}
|
|
|
|
// Add the new device cgroup rule.
|
|
err := d.CGroupSet(rule.Key, rule.Value)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to add cgroup rule for device: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// deviceAttachNIC live attaches a NIC device to a container.
|
|
func (d *lxc) deviceAttachNIC(devName string, configCopy map[string]string, netIF []deviceConfig.RunConfigItem) error {
|
|
ctDevName := ""
|
|
connected := true
|
|
for _, dev := range netIF {
|
|
if dev.Key == "link" {
|
|
ctDevName = dev.Value
|
|
} else if dev.Key == "connected" {
|
|
connected = util.IsTrueOrEmpty(dev.Value)
|
|
}
|
|
}
|
|
|
|
if ctDevName == "" {
|
|
return errors.New("Device didn't provide a link property to use")
|
|
}
|
|
|
|
// Load the go-lxc struct.
|
|
cc, err := d.initLXC(false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add the interface to the container.
|
|
err = cc.AttachInterface(ctDevName, configCopy["name"])
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to attach interface: %s to %s: %w", ctDevName, configCopy["name"], err)
|
|
}
|
|
|
|
return d.setNICLink(devName, connected, true)
|
|
}
|
|
|
|
// deviceStop loads a new device and calls its Stop() function.
|
|
// Accepts a stopHookNetnsPath argument which is required when run from the onStopNS hook before the
|
|
// container's network namespace is unmounted (which is required for NIC device cleanup).
|
|
func (d *lxc) deviceStop(dev device.Device, instanceRunning bool, stopHookNetnsPath string) error {
|
|
configCopy := dev.Config()
|
|
l := d.logger.AddContext(logger.Ctx{"device": dev.Name(), "type": configCopy["type"]})
|
|
l.Debug("Stopping device")
|
|
|
|
if instanceRunning && !dev.CanHotPlug() {
|
|
return errors.New("Device cannot be stopped when instance is running")
|
|
}
|
|
|
|
runConf, err := dev.Stop()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if runConf != nil {
|
|
// If network interface settings returned, then detach NIC from container.
|
|
if len(runConf.NetworkInterface) > 0 {
|
|
err = d.deviceDetachNIC(configCopy, runConf.NetworkInterface, instanceRunning, stopHookNetnsPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Add cgroup rules if requested and container is running.
|
|
if len(runConf.CGroups) > 0 && instanceRunning {
|
|
err = d.deviceAddCgroupRules(runConf.CGroups)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Detach mounts if requested and container is running.
|
|
if len(runConf.Mounts) > 0 && instanceRunning {
|
|
err = d.deviceHandleMounts(runConf.Mounts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Run post stop hooks irrespective of run state of instance.
|
|
err = d.runHooks(runConf.PostHooks)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// deviceDetachNIC detaches a NIC device from a container.
|
|
// Accepts a stopHookNetnsPath argument which is required when run from the onStopNS hook before the
|
|
// container's network namespace is unmounted (which is required for NIC device cleanup).
|
|
func (d *lxc) deviceDetachNIC(configCopy map[string]string, netIF []deviceConfig.RunConfigItem, instanceRunning bool, stopHookNetnsPath string) error {
|
|
// Get requested device name to detach interface back to on the host.
|
|
devName := ""
|
|
for _, dev := range netIF {
|
|
if dev.Key == "link" {
|
|
devName = dev.Value
|
|
break
|
|
}
|
|
}
|
|
|
|
if devName == "" {
|
|
return errors.New("Device didn't provide a link property to use")
|
|
}
|
|
|
|
// If container is running, perform live detach of interface back to host.
|
|
if instanceRunning {
|
|
// For some reason, having network config confuses detach, so get our own go-lxc struct.
|
|
cname := project.Instance(d.Project().Name, d.Name())
|
|
cc, err := liblxc.NewContainer(cname, d.state.OS.LxcPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() { _ = cc.Release() }()
|
|
|
|
// Get interfaces inside container.
|
|
ifaces, err := cc.Interfaces()
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to list network interfaces: %w", err)
|
|
}
|
|
|
|
// If interface doesn't exist inside container, cannot proceed.
|
|
if !slices.Contains(ifaces, configCopy["name"]) {
|
|
return nil
|
|
}
|
|
|
|
err = cc.DetachInterfaceRename(configCopy["name"], devName)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to detach interface: %q to %q: %w", configCopy["name"], devName, err)
|
|
}
|
|
} else {
|
|
// Currently liblxc does not move devices back to the host on stop that were added
|
|
// after the container was started. For this reason we utilise the lxc.hook.stop
|
|
// hook so that we can capture the netns path, enter the namespace and move the nics
|
|
// back to the host and rename them if liblxc hasn't already done it.
|
|
// We can only move back devices that have an expected host_name record and where
|
|
// that device doesn't already exist on the host as if a device exists on the host
|
|
// we can't know whether that is because liblxc has moved it back already or whether
|
|
// it is a conflicting device.
|
|
if !util.PathExists(fmt.Sprintf("/sys/class/net/%s", devName)) {
|
|
if stopHookNetnsPath == "" {
|
|
return fmt.Errorf("Cannot detach NIC device %q without stopHookNetnsPath being provided", devName)
|
|
}
|
|
|
|
err := d.detachInterfaceRename(stopHookNetnsPath, configCopy["name"], devName)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to detach interface: %q to %q: %w", configCopy["name"], devName, err)
|
|
}
|
|
|
|
d.logger.Debug("Detached NIC device interface", logger.Ctx{"name": configCopy["name"], "device": devName})
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// deviceHandleMounts live attaches or detaches mounts on a container.
|
|
// If the mount DevPath is empty the mount action is treated as unmount.
|
|
func (d *lxc) deviceHandleMounts(mounts []deviceConfig.MountEntryItem) error {
|
|
for _, mount := range mounts {
|
|
if mount.DevPath != "" {
|
|
flags := 0
|
|
|
|
// Convert options into flags.
|
|
for _, opt := range mount.Opts {
|
|
if opt == "bind" {
|
|
flags |= unix.MS_BIND
|
|
} else if opt == "rbind" {
|
|
flags |= unix.MS_BIND | unix.MS_REC
|
|
} else if opt == "ro" {
|
|
flags |= unix.MS_RDONLY
|
|
}
|
|
}
|
|
|
|
var idmapType idmap.StorageType = idmap.StorageTypeNone
|
|
if !d.IsPrivileged() && mount.OwnerShift == deviceConfig.MountOwnerShiftDynamic {
|
|
idmapType = d.IdmappedStorage(mount.DevPath, mount.FSType)
|
|
if idmapType == idmap.StorageTypeNone {
|
|
return errors.New("Required idmapping abilities not available")
|
|
}
|
|
}
|
|
|
|
// Mount it into the container.
|
|
err := d.insertMount(mount.DevPath, mount.TargetPath, mount.FSType, flags, idmapType)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to add mount for device inside container: %s", err)
|
|
}
|
|
} else {
|
|
relativeTargetPath := strings.TrimPrefix(mount.TargetPath, "/")
|
|
|
|
// Connect to files API.
|
|
files, err := d.FileSFTP()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() { _ = files.Close() }()
|
|
|
|
_, err = files.Lstat(relativeTargetPath)
|
|
if err == nil {
|
|
err := d.removeMount(mount.TargetPath)
|
|
if err != nil {
|
|
return fmt.Errorf("Error unmounting the device path inside container: %s", err)
|
|
}
|
|
|
|
// Only remove mountpoints created in /dev.
|
|
if strings.HasPrefix(mount.TargetPath, "dev/") {
|
|
err := files.Remove(relativeTargetPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeviceEventHandler actions the results of a RunConfig after an event has occurred on a device.
|
|
func (d *lxc) DeviceEventHandler(runConf *deviceConfig.RunConfig) error {
|
|
// Device events can only be processed when the container is running.
|
|
// We use InitPID here rather than IsRunning because this task can be triggered during the
|
|
// container startup process, which is during the time that the start lock is held, which causes
|
|
// IsRunning to return false (because the container hasn't fully started yet).
|
|
if d.InitPID() <= 0 {
|
|
return nil
|
|
}
|
|
|
|
if runConf == nil {
|
|
return nil
|
|
}
|
|
|
|
// Shift device file ownership if needed before mounting devices into container.
|
|
if len(runConf.Mounts) > 0 {
|
|
err := d.deviceStaticShiftMounts(runConf.Mounts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = d.deviceHandleMounts(runConf.Mounts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Add cgroup rules if requested.
|
|
if len(runConf.CGroups) > 0 {
|
|
err := d.deviceAddCgroupRules(runConf.CGroups)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Handle NIC reconfiguration.
|
|
var devName string
|
|
var connected bool
|
|
for _, dev := range runConf.NetworkInterface {
|
|
switch dev.Key {
|
|
case "devName":
|
|
devName = dev.Value
|
|
case "connected":
|
|
connected = util.IsTrueOrEmpty(dev.Value)
|
|
}
|
|
}
|
|
|
|
if devName != "" {
|
|
err := d.setNICLink(devName, connected, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Run any post hooks requested.
|
|
err := d.runHooks(runConf.PostHooks)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Generate uevent inside container if requested.
|
|
if len(runConf.Uevents) > 0 {
|
|
pidFd := d.inheritInitPidFd()
|
|
pidFdNr := "-1"
|
|
if pidFd != nil {
|
|
defer func() { _ = pidFd.Close() }()
|
|
pidFdNr = "3"
|
|
}
|
|
|
|
for _, eventParts := range runConf.Uevents {
|
|
length := 0
|
|
for _, part := range eventParts {
|
|
length = length + len(part) + 1
|
|
}
|
|
|
|
args := []string{
|
|
"forkuevent",
|
|
"inject",
|
|
"--",
|
|
fmt.Sprintf("%d", d.InitPID()),
|
|
pidFdNr,
|
|
fmt.Sprintf("%d", length),
|
|
}
|
|
|
|
args = append(args, eventParts...)
|
|
|
|
_, _, err := subprocess.RunCommandSplit(
|
|
context.TODO(),
|
|
nil,
|
|
[]*os.File{pidFd},
|
|
d.state.OS.ExecPath,
|
|
args...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *lxc) handleIdmappedStorage() (idmap.StorageType, *idmap.Set, error) {
|
|
diskIdmap, err := d.DiskIdmap()
|
|
if err != nil {
|
|
return idmap.StorageTypeNone, nil, fmt.Errorf("Set last ID map: %w", err)
|
|
}
|
|
|
|
nextIdmap, err := d.NextIdmap()
|
|
if err != nil {
|
|
return idmap.StorageTypeNone, nil, fmt.Errorf("Set ID map: %w", err)
|
|
}
|
|
|
|
// Identical on-disk idmaps so no changes required.
|
|
if nextIdmap.Equals(diskIdmap) {
|
|
return idmap.StorageTypeNone, nextIdmap, nil
|
|
}
|
|
|
|
// There's no on-disk idmap applied and the container can use idmapped
|
|
// storage.
|
|
idmapType := d.IdmappedStorage(d.RootfsPath(), "none")
|
|
if diskIdmap == nil && idmapType != idmap.StorageTypeNone {
|
|
return idmapType, nextIdmap, nil
|
|
}
|
|
|
|
// We need to change the on-disk idmap but the container is protected
|
|
// against idmap changes.
|
|
if util.IsTrue(d.expandedConfig["security.protection.shift"]) {
|
|
return idmap.StorageTypeNone, nil, errors.New("Container is protected against filesystem shifting")
|
|
}
|
|
|
|
d.logger.Debug("Container idmap changed, remapping")
|
|
d.updateProgress("Remapping container filesystem")
|
|
|
|
storageType, err := d.getStorageType()
|
|
if err != nil {
|
|
return idmap.StorageTypeNone, nil, fmt.Errorf("Storage type: %w", err)
|
|
}
|
|
|
|
// Revert the currently applied on-disk idmap.
|
|
if diskIdmap != nil {
|
|
if storageType == "zfs" {
|
|
err = diskIdmap.UnshiftPath(d.RootfsPath(), storageDrivers.ShiftZFSSkipper)
|
|
} else if storageType == "btrfs" {
|
|
err = storageDrivers.UnshiftBtrfsRootfs(d.RootfsPath(), diskIdmap)
|
|
} else {
|
|
err = diskIdmap.UnshiftPath(d.RootfsPath(), nil)
|
|
}
|
|
|
|
if err != nil {
|
|
return idmap.StorageTypeNone, nil, err
|
|
}
|
|
}
|
|
|
|
jsonDiskIdmap := "[]"
|
|
|
|
// If the container can't use idmapped storage apply the new on-disk
|
|
// idmap of the container now. Otherwise we will later instruct LXC to
|
|
// make use of idmapped storage.
|
|
if nextIdmap != nil && idmapType == idmap.StorageTypeNone {
|
|
if storageType == "zfs" {
|
|
err = nextIdmap.ShiftPath(d.RootfsPath(), storageDrivers.ShiftZFSSkipper)
|
|
} else if storageType == "btrfs" {
|
|
err = storageDrivers.ShiftBtrfsRootfs(d.RootfsPath(), nextIdmap)
|
|
} else {
|
|
err = nextIdmap.ShiftPath(d.RootfsPath(), nil)
|
|
}
|
|
|
|
if err != nil {
|
|
return idmap.StorageTypeNone, nil, err
|
|
}
|
|
|
|
idmapJSON, err := nextIdmap.ToJSON()
|
|
if err != nil {
|
|
return idmap.StorageTypeNone, nil, err
|
|
}
|
|
|
|
jsonDiskIdmap = idmapJSON
|
|
}
|
|
|
|
err = d.VolatileSet(map[string]string{"volatile.last_state.idmap": jsonDiskIdmap})
|
|
if err != nil {
|
|
return idmap.StorageTypeNone, nextIdmap, fmt.Errorf("Set volatile.last_state.idmap config key on container %q (id %d): %w", d.name, d.id, err)
|
|
}
|
|
|
|
d.updateProgress("")
|
|
return idmapType, nextIdmap, nil
|
|
}
|
|
|
|
// Start functions.
|
|
func (d *lxc) startCommon() (string, []func() error, error) {
|
|
postStartHooks := []func() error{}
|
|
|
|
reverter := revert.New()
|
|
defer reverter.Fail()
|
|
|
|
// Assign NUMA node(s) if needed.
|
|
if d.expandedConfig["limits.cpu.nodes"] == "balanced" {
|
|
err := d.balanceNUMANodes()
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
}
|
|
|
|
// Check if idmap needs changing.
|
|
if !d.IsPrivileged() {
|
|
nextMap, err := d.NextIdmap()
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
// Check if we need to change idmap.
|
|
if nextMap != nil && d.state.OS.IdmapSet != nil && !d.state.OS.IdmapSet.Includes(nextMap) {
|
|
// Update the idmap.
|
|
idmapSet, base, err := d.findIdmap()
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("Failed to get ID map: %w", err)
|
|
}
|
|
|
|
idmapSetJSON, err := idmapSet.ToJSON()
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("Failed to encode ID map: %w", err)
|
|
}
|
|
|
|
err = d.VolatileSet(map[string]string{
|
|
"volatile.idmap.next": idmapSetJSON,
|
|
"volatile.idmap.base": fmt.Sprintf("%v", base),
|
|
})
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("Failed to update volatile idmap: %w", err)
|
|
}
|
|
|
|
// Invalidate the idmap cache.
|
|
d.idmapset = nil
|
|
}
|
|
}
|
|
|
|
// Load the go-lxc struct
|
|
cc, err := d.initLXC(true)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("Load go-lxc struct: %w", err)
|
|
}
|
|
|
|
// gendoc:generate(entity=image, group=requirements, key=requirements.cgroup)
|
|
//
|
|
// ---
|
|
// type: string
|
|
// shortdesc: If set to `v1`, indicates that the image requires the host to run cgroup v1.
|
|
//
|
|
// Ensure cgroup v1 configuration is set appropriately with the image using systemd
|
|
if d.localConfig["image.requirements.cgroup"] == "v1" && !util.PathExists("/sys/fs/cgroup/systemd") {
|
|
return "", nil, errors.New("The image used by this instance requires a CGroupV1 host system")
|
|
}
|
|
|
|
// gendoc:generate(entity=image, group=requirements, key=requirements.privileged)
|
|
//
|
|
// ---
|
|
// type: bool
|
|
// shortdesc: If set to `false`, indicates that the image cannot work as a privileged container.
|
|
//
|
|
// Ensure privileged is turned off for images that cannot work privileged
|
|
if util.IsFalse(d.localConfig["image.requirements.privileged"]) && util.IsTrue(d.expandedConfig["security.privileged"]) {
|
|
return "", nil, errors.New("The image used by this instance is incompatible with privileged containers. Please unset security.privileged on the instance")
|
|
}
|
|
|
|
// Load any required kernel modules
|
|
kernelModules := d.expandedConfig["linux.kernel_modules"]
|
|
if kernelModules != "" {
|
|
for _, module := range strings.Split(kernelModules, ",") {
|
|
module = strings.TrimPrefix(module, " ")
|
|
err := linux.LoadModule(module)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("Failed to load kernel module '%s': %w", module, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Rotate the log file.
|
|
logfile := d.LogFilePath()
|
|
if util.PathExists(logfile) {
|
|
_ = os.Remove(logfile + ".old")
|
|
err := os.Rename(logfile, logfile+".old")
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return "", nil, err
|
|
}
|
|
}
|
|
|
|
// Wait for any file operations to complete.
|
|
// This is to avoid having an active mount by forkfile and so all file operations
|
|
// from this point will use the container's namespace rather than a chroot.
|
|
d.stopForkfile(false)
|
|
|
|
// Mount instance root volume.
|
|
mountInfo, err := d.mount()
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
// Handle post hooks.
|
|
postStartHooks = append(postStartHooks, func() error {
|
|
for _, hook := range mountInfo.PostHooks {
|
|
err := hook(d)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
reverter.Add(func() { _ = d.unmount() })
|
|
|
|
idmapType, nextIdmap, err := d.handleIdmappedStorage()
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("Failed to handle idmapped storage: %w", err)
|
|
}
|
|
|
|
nextIdmapJSON, err := nextIdmap.ToJSON()
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("Failed to encode ID map: %w", err)
|
|
}
|
|
|
|
if d.localConfig["volatile.idmap.current"] != nextIdmapJSON {
|
|
err = d.VolatileSet(map[string]string{"volatile.idmap.current": nextIdmapJSON})
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("Set volatile.idmap.current config key on container %q (id %d): %w", d.name, d.id, err)
|
|
}
|
|
}
|
|
|
|
// Generate the Seccomp profile
|
|
err = seccomp.CreateProfile(d.state, d)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
// Cleanup any existing leftover devices
|
|
_ = d.removeUnixDevices()
|
|
_ = d.removeDiskDevices()
|
|
|
|
// Create any missing directories.
|
|
err = os.MkdirAll(d.LogPath(), 0o700)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
err = os.MkdirAll(d.RunPath(), 0o700)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
err = os.MkdirAll(d.DevicesPath(), 0o711)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
err = os.MkdirAll(d.ShmountsPath(), 0o711)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
volatileSet := make(map[string]string)
|
|
|
|
// Generate UUID if not present (do this before UpdateBackupFile() call).
|
|
instUUID := d.localConfig["volatile.uuid"]
|
|
if instUUID == "" {
|
|
instUUID = uuid.New().String()
|
|
volatileSet["volatile.uuid"] = instUUID
|
|
}
|
|
|
|
// For a container instance, we must also set the generation UUID.
|
|
genUUID := d.localConfig["volatile.uuid.generation"]
|
|
if genUUID == "" {
|
|
genUUID = instUUID
|
|
volatileSet["volatile.uuid.generation"] = genUUID
|
|
}
|
|
|
|
// Apply any volatile changes that need to be made.
|
|
err = d.VolatileSet(volatileSet)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("Failed setting volatile keys: %w", err)
|
|
}
|
|
|
|
// Create the devices
|
|
nicID := -1
|
|
nvidiaDevices := []string{}
|
|
|
|
sortedDevices := d.expandedDevices.Sorted()
|
|
startDevices := make([]device.Device, 0, len(sortedDevices))
|
|
|
|
// Load devices in sorted order, this ensures that device mounts are added in path order.
|
|
// Loading all devices first means that validation of all devices occurs before starting any of them.
|
|
for _, entry := range sortedDevices {
|
|
dev, err := d.deviceLoad(d, entry.Name, entry.Config)
|
|
if err != nil {
|
|
if errors.Is(err, device.ErrUnsupportedDevType) {
|
|
continue // Skip unsupported device (allows for mixed instance type profiles).
|
|
}
|
|
|
|
return "", nil, fmt.Errorf("Failed start validation for device %q: %w", entry.Name, err)
|
|
}
|
|
|
|
// Run pre-start of check all devices before starting any device to avoid expensive revert.
|
|
err = dev.PreStartCheck()
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("Failed pre-start check for device %q: %w", dev.Name(), err)
|
|
}
|
|
|
|
startDevices = append(startDevices, dev)
|
|
}
|
|
|
|
// Start devices in order.
|
|
for i := range startDevices {
|
|
dev := startDevices[i] // Local var for revert.
|
|
|
|
// Start the device.
|
|
runConf, err := d.deviceStart(dev, false)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("Failed to start device %q: %w", dev.Name(), err)
|
|
}
|
|
|
|
// Stop device on failure to setup container.
|
|
reverter.Add(func() {
|
|
err := d.deviceStop(dev, false, "")
|
|
if err != nil {
|
|
d.logger.Error("Failed to cleanup device", logger.Ctx{"device": dev.Name(), "err": err})
|
|
}
|
|
})
|
|
|
|
if runConf == nil {
|
|
continue
|
|
}
|
|
|
|
if runConf.Revert != nil {
|
|
reverter.Add(runConf.Revert)
|
|
}
|
|
|
|
// Process rootfs setup.
|
|
if runConf.RootFS.Path != "" {
|
|
if !liblxc.RuntimeLiblxcVersionAtLeast(liblxc.Version(), 2, 1, 0) {
|
|
// Set the rootfs backend type if supported (must happen before any other lxc.rootfs)
|
|
err := lxcSetConfigItem(cc, "lxc.rootfs.backend", "dir")
|
|
if err == nil {
|
|
value := cc.ConfigItem("lxc.rootfs.backend")
|
|
if len(value) == 0 || value[0] != "dir" {
|
|
_ = lxcSetConfigItem(cc, "lxc.rootfs.backend", "")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get an absolute path for the rootfs (avoid constantly traversing the symlink).
|
|
absoluteRootfs, err := filepath.EvalSymlinks(runConf.RootFS.Path)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("Unable to resolve container rootfs: %w", err)
|
|
}
|
|
|
|
if liblxc.RuntimeLiblxcVersionAtLeast(liblxc.Version(), 2, 1, 0) {
|
|
rootfsPath := fmt.Sprintf("dir:%s", absoluteRootfs)
|
|
err = lxcSetConfigItem(cc, "lxc.rootfs.path", rootfsPath)
|
|
} else {
|
|
err = lxcSetConfigItem(cc, "lxc.rootfs", absoluteRootfs)
|
|
}
|
|
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("Failed to setup device rootfs %q: %w", dev.Name(), err)
|
|
}
|
|
|
|
if len(runConf.RootFS.Opts) > 0 {
|
|
err = lxcSetConfigItem(cc, "lxc.rootfs.options", strings.Join(runConf.RootFS.Opts, ","))
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("Failed to setup device rootfs %q: %w", dev.Name(), err)
|
|
}
|
|
}
|
|
|
|
if !d.IsPrivileged() && idmapType == idmap.StorageTypeIdmapped {
|
|
err = lxcSetConfigItem(cc, "lxc.rootfs.options", "idmap=container")
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("Failed to set \"idmap=container\" rootfs option: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pass any cgroups rules into LXC.
|
|
if len(runConf.CGroups) > 0 {
|
|
for _, rule := range runConf.CGroups {
|
|
if strings.HasPrefix(rule.Key, "devices.") && (!d.isCurrentlyPrivileged() || d.state.OS.RunningInUserNS) {
|
|
continue
|
|
}
|
|
|
|
if d.state.OS.CGInfo.Layout == cgroup.CgroupsUnified {
|
|
err = lxcSetConfigItem(cc, fmt.Sprintf("lxc.cgroup2.%s", rule.Key), rule.Value)
|
|
} else {
|
|
err = lxcSetConfigItem(cc, fmt.Sprintf("lxc.cgroup.%s", rule.Key), rule.Value)
|
|
}
|
|
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("Failed to setup device cgroup %q: %w", dev.Name(), err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pass any mounts into LXC.
|
|
if len(runConf.Mounts) > 0 {
|
|
escapePathFstab := func(path string) string {
|
|
r := strings.NewReplacer(
|
|
" ", "\\040",
|
|
"\t", "\\011",
|
|
"\n", "\\012",
|
|
"\\", "\\\\")
|
|
return r.Replace(path)
|
|
}
|
|
|
|
for _, mount := range runConf.Mounts {
|
|
if slices.Contains(mount.Opts, "propagation") && !liblxc.RuntimeLiblxcVersionAtLeast(liblxc.Version(), 3, 0, 0) {
|
|
return "", nil, fmt.Errorf("Failed to setup device mount %q: %w", dev.Name(), errors.New("liblxc 3.0 is required for mount propagation configuration"))
|
|
}
|
|
|
|
mntOptions := strings.Join(mount.Opts, ",")
|
|
|
|
if !d.IsPrivileged() && mount.OwnerShift == deviceConfig.MountOwnerShiftDynamic {
|
|
switch d.IdmappedStorage(mount.DevPath, mount.FSType) {
|
|
case idmap.StorageTypeIdmapped:
|
|
mntOptions = strings.Join([]string{mntOptions, "idmap=container"}, ",")
|
|
case idmap.StorageTypeNone:
|
|
return "", nil, fmt.Errorf("Failed to setup device mount %q: %w", dev.Name(), errors.New("idmapping abilities are required but aren't supported on system"))
|
|
}
|
|
}
|
|
|
|
mntVal := fmt.Sprintf("%s %s %s %s %d %d", escapePathFstab(mount.DevPath), escapePathFstab(mount.TargetPath), mount.FSType, mntOptions, mount.Freq, mount.PassNo)
|
|
err = lxcSetConfigItem(cc, "lxc.mount.entry", mntVal)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("Failed to setup device mount %q: %w", dev.Name(), err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pass any network setup config into LXC.
|
|
if len(runConf.NetworkInterface) > 0 {
|
|
// Increment nicID so that LXC network index is unique per device.
|
|
nicID++
|
|
|
|
networkKeyPrefix := "lxc.net"
|
|
if !liblxc.RuntimeLiblxcVersionAtLeast(liblxc.Version(), 2, 1, 0) {
|
|
networkKeyPrefix = "lxc.network"
|
|
}
|
|
|
|
for _, nicItem := range runConf.NetworkInterface {
|
|
// The connected state is not a LXC configuration key; we defer its handling to a post hook.
|
|
if nicItem.Key == "connected" {
|
|
runConf.PostHooks = append(runConf.PostHooks, func() error {
|
|
return d.setNICLink(dev.Name(), util.IsTrueOrEmpty(nicItem.Value), true)
|
|
})
|
|
|
|
continue
|
|
}
|
|
|
|
err = lxcSetConfigItem(cc, fmt.Sprintf("%s.%d.%s", networkKeyPrefix, nicID, nicItem.Key), nicItem.Value)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("Failed to setup device network interface %q: %w", dev.Name(), err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add any post start hooks.
|
|
if len(runConf.PostHooks) > 0 {
|
|
postStartHooks = append(postStartHooks, runConf.PostHooks...)
|
|
}
|
|
|
|
// Build list of NVIDIA GPUs (used for MIG).
|
|
if len(runConf.GPUDevice) > 0 {
|
|
for _, entry := range runConf.GPUDevice {
|
|
if entry.Key == device.GPUNvidiaDeviceKey {
|
|
nvidiaDevices = append(nvidiaDevices, entry.Value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize the credentials directory.
|
|
err = d.setupCredentials(false)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
// Override NVIDIA_VISIBLE_DEVICES if we have devices that need it.
|
|
if len(nvidiaDevices) > 0 {
|
|
err = lxcSetConfigItem(cc, "lxc.environment", fmt.Sprintf("NVIDIA_VISIBLE_DEVICES=%s", strings.Join(nvidiaDevices, ",")))
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("Unable to set NVIDIA_VISIBLE_DEVICES in LXC environment: %w", err)
|
|
}
|
|
}
|
|
|
|
// Check if we should start a dedicated LXCFS.
|
|
if d.state.GlobalConfig.InstancesLXCFSPerInstance() {
|
|
if !util.PathExists(filepath.Join(d.RunPath(), "lxcfs", "proc")) {
|
|
// Make sure all the paths exist.
|
|
err := os.Mkdir(filepath.Join(d.DevicesPath(), "lxcfs"), 0o711)
|
|
if err != nil && !os.IsExist(err) {
|
|
return "", nil, err
|
|
}
|
|
|
|
err = os.Mkdir(filepath.Join(d.RunPath(), "lxcfs"), 0o700)
|
|
if err != nil && !os.IsExist(err) {
|
|
return "", nil, err
|
|
}
|
|
|
|
// Prepare a new LXCFS instance.
|
|
args := []string{
|
|
"-f",
|
|
"-p", filepath.Join(d.RunPath(), "lxcfs.pid"),
|
|
"--runtime-dir", filepath.Join(d.RunPath(), "lxcfs"),
|
|
}
|
|
|
|
if os.Getenv("LXCFS_OPTS") != "" {
|
|
userArgs, err := shellquote.Split(os.Getenv("LXCFS_OPTS"))
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
args = append(args, userArgs...)
|
|
}
|
|
|
|
args = append(args, filepath.Join(d.DevicesPath(), "lxcfs"))
|
|
|
|
lxcfs, err := subprocess.NewProcess("lxcfs", args, "", "")
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
// Start LXCFS.
|
|
err = lxcfs.Start(context.TODO())
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
// Write down our process tracking.
|
|
err = lxcfs.Save(filepath.Join(d.RunPath(), "lxcfs.yaml"))
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
}
|
|
|
|
// Over-mount the system LXCFS (if found).
|
|
for _, entry := range []string{"/var/lib/lxcfs", "/var/lib/incus-lxcfs"} {
|
|
if !util.PathExists(entry) {
|
|
continue
|
|
}
|
|
|
|
err = lxcSetConfigItem(cc, "lxc.hook.pre-mount", fmt.Sprintf("mount -o bind %s %s/", filepath.Join(d.DevicesPath(), "lxcfs"), entry))
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Setup BPF token delegation if enabled
|
|
bpfConfig := d.bpfTokenConfig()
|
|
if bpfConfig.enable {
|
|
err = lxcSetConfigItem(cc, "lxc.hook.start-host", shellquote.Join(
|
|
fmt.Sprintf("/proc/%d/exe", os.Getpid()),
|
|
"forkbpf",
|
|
bpfConfig.mountPath,
|
|
bpfConfig.cmdTypes,
|
|
bpfConfig.mapTypes,
|
|
bpfConfig.progTypes,
|
|
bpfConfig.attachTypes,
|
|
))
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
}
|
|
|
|
// Load the LXC raw config.
|
|
err = d.loadRawLXCConfig(cc)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
// Generate the LXC config
|
|
configPath := filepath.Join(d.RunPath(), "lxc.conf")
|
|
err = cc.SaveConfigFile(configPath)
|
|
if err != nil {
|
|
_ = os.Remove(configPath)
|
|
return "", nil, err
|
|
}
|
|
|
|
// Set ownership to match container root
|
|
currentIdmapset, err := d.CurrentIdmap()
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
uid := int64(0)
|
|
if currentIdmapset != nil {
|
|
uid, _ = currentIdmapset.ShiftFromNS(0, 0)
|
|
}
|
|
|
|
err = os.Chown(d.Path(), int(uid), 0)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
// We only need traversal by root in the container
|
|
err = os.Chmod(d.Path(), 0o100)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
// If starting stateless, wipe state
|
|
if !d.IsStateful() && util.PathExists(d.StatePath()) {
|
|
_ = os.RemoveAll(d.StatePath())
|
|
}
|
|
|
|
// Snapshot if needed.
|
|
snapName, expiry, err := d.getStartupSnapNameAndExpiry(d)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("Failed getting startup snapshot info: %w", err)
|
|
}
|
|
|
|
if snapName != "" && expiry != nil {
|
|
err := d.snapshot(snapName, *expiry, false)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("Failed taking startup snapshot: %w", err)
|
|
}
|
|
}
|
|
|
|
// Update the backup.yaml file just before starting the instance process, but after all devices have been
|
|
// setup, so that the backup file contains the volatile keys used for this instance start, so that they
|
|
// can be used for instance cleanup.
|
|
err = d.UpdateBackupFile()
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
reverter.Success()
|
|
|
|
return configPath, postStartHooks, nil
|
|
}
|
|
|
|
type bpfTokenConfig struct {
|
|
enable bool
|
|
mountPath string
|
|
cmdTypes string
|
|
mapTypes string
|
|
progTypes string
|
|
attachTypes string
|
|
}
|
|
|
|
func (d *lxc) bpfTokenConfig() bpfTokenConfig {
|
|
if util.IsTrue(d.expandedConfig["security.privileged"]) {
|
|
return bpfTokenConfig{}
|
|
}
|
|
|
|
cfg := bpfTokenConfig{
|
|
mountPath: d.expandedConfig["security.bpffs.path"],
|
|
cmdTypes: d.expandedConfig["security.bpffs.delegate_cmds"],
|
|
mapTypes: d.expandedConfig["security.bpffs.delegate_maps"],
|
|
progTypes: d.expandedConfig["security.bpffs.delegate_progs"],
|
|
attachTypes: d.expandedConfig["security.bpffs.delegate_attachs"],
|
|
}
|
|
|
|
if cfg.cmdTypes != "" || cfg.mapTypes != "" || cfg.progTypes != "" || cfg.attachTypes != "" {
|
|
cfg.enable = true
|
|
}
|
|
|
|
if cfg.mountPath == "" {
|
|
cfg.mountPath = "/sys/fs/bpf"
|
|
}
|
|
|
|
return cfg
|
|
}
|
|
|
|
// detachInterfaceRename enters the container's network namespace and moves the named interface
|
|
// in ifName back to the network namespace of the running process as the name specified in hostName.
|
|
func (d *lxc) detachInterfaceRename(netns string, ifName string, hostName string) error {
|
|
daemonPID := os.Getpid()
|
|
|
|
// Run forknet detach
|
|
_, err := subprocess.RunCommand(
|
|
d.state.OS.ExecPath,
|
|
"forknet",
|
|
"detach",
|
|
"--",
|
|
netns,
|
|
fmt.Sprintf("%d", daemonPID),
|
|
ifName,
|
|
hostName,
|
|
)
|
|
// Process forknet detach response
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Start starts the instance.
|
|
func (d *lxc) Start(stateful bool) error {
|
|
// Check that migration.stateful is set for stateful actions.
|
|
if stateful && !d.CanLiveMigrate() {
|
|
return errors.New("Stateful start requires that the instance migration.stateful be set to true")
|
|
}
|
|
|
|
d.logger.Debug("Start started", logger.Ctx{"stateful": stateful})
|
|
defer d.logger.Debug("Start finished", logger.Ctx{"stateful": stateful})
|
|
|
|
// Check that we are startable before creating an operation lock.
|
|
// Must happen before creating operation Start lock to avoid the status check returning Stopped due to the
|
|
// existence of a Start operation lock.
|
|
err := d.validateStartup(stateful, d.statusCode())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Setup a new operation.
|
|
op, err := operationlock.CreateWaitGet(d.Project().Name, d.Name(), d.op, operationlock.ActionStart, []operationlock.Action{operationlock.ActionRestart, operationlock.ActionRestore}, false, false)
|
|
if err != nil {
|
|
if errors.Is(err, operationlock.ErrNonReusuableSucceeded) {
|
|
// An existing matching operation has now succeeded, return.
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("Failed to create instance start operation: %w", err)
|
|
}
|
|
|
|
defer op.Done(nil)
|
|
|
|
if !daemon.SharedMountsSetup {
|
|
err = errors.New("Daemon failed to setup shared mounts base. Does security.nesting need to be turned on?")
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
ctxMap := logger.Ctx{
|
|
"action": op.Action(),
|
|
"created": d.creationDate,
|
|
"ephemeral": d.ephemeral,
|
|
"used": d.lastUsedDate,
|
|
"stateful": stateful,
|
|
}
|
|
|
|
if op.Action() == "start" {
|
|
d.logger.Info("Starting instance", ctxMap)
|
|
}
|
|
|
|
// If stateful, restore now.
|
|
if stateful && d.stateful {
|
|
d.logger.Info("Restoring stateful checkpoint")
|
|
|
|
criuMigrationArgs := instance.CriuMigrationArgs{
|
|
Cmd: liblxc.MIGRATE_RESTORE,
|
|
StateDir: d.StatePath(),
|
|
Function: "snapshot",
|
|
Stop: false,
|
|
ActionScript: false,
|
|
DumpDir: "",
|
|
PreDumpDir: "",
|
|
}
|
|
|
|
err = d.migrate(&criuMigrationArgs)
|
|
if err != nil && !d.IsRunning() {
|
|
op.Done(err)
|
|
return fmt.Errorf("Failed restoring stateful checkpoint: %w", err)
|
|
}
|
|
|
|
_ = os.RemoveAll(d.StatePath())
|
|
d.stateful = false
|
|
|
|
err = d.state.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
|
|
return tx.UpdateInstanceStatefulFlag(ctx, d.id, false)
|
|
})
|
|
if err != nil {
|
|
op.Done(err)
|
|
return fmt.Errorf("Failed clearing instance stateful flag: %w", err)
|
|
}
|
|
|
|
if op.Action() == "start" {
|
|
d.logger.Info("Started instance", ctxMap)
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceStarted.Event(d, nil))
|
|
}
|
|
|
|
return nil
|
|
} else if d.stateful {
|
|
/* stateless start required when we have state, let's delete it */
|
|
err := os.RemoveAll(d.StatePath())
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
d.stateful = false
|
|
err = d.state.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
|
|
return tx.UpdateInstanceStatefulFlag(ctx, d.id, false)
|
|
})
|
|
if err != nil {
|
|
op.Done(err)
|
|
return fmt.Errorf("Failed clearing instance stateful flag: %w", err)
|
|
}
|
|
}
|
|
|
|
// Run the shared start code.
|
|
configPath, postStartHooks, err := d.startCommon()
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
name := project.Instance(d.Project().Name, d.name)
|
|
|
|
// Setup minimal environment for forkstart.
|
|
envDict := map[string]string{
|
|
"container": "lxc",
|
|
}
|
|
|
|
for k, v := range d.expandedConfig {
|
|
after, ok := strings.CutPrefix(k, "environment.")
|
|
if ok {
|
|
envDict[after] = v
|
|
}
|
|
}
|
|
|
|
for _, keepEnv := range []string{"LD_LIBRARY_PATH", "INCUS_DIR", "INCUS_SOCKET"} {
|
|
if os.Getenv(keepEnv) != "" {
|
|
envDict[keepEnv] = os.Getenv(keepEnv)
|
|
}
|
|
}
|
|
|
|
_, ok := envDict["PATH"]
|
|
if !ok {
|
|
envDict["PATH"] = os.Getenv("PATH")
|
|
}
|
|
|
|
env := make([]string, 0, len(envDict))
|
|
for k, v := range envDict {
|
|
env = append(env, fmt.Sprintf("%s=%s", k, v))
|
|
}
|
|
|
|
// Start the LXC container.
|
|
_, _, err = subprocess.RunCommandSplit(
|
|
context.TODO(),
|
|
env,
|
|
nil,
|
|
d.state.OS.ExecPath,
|
|
"forkstart",
|
|
name,
|
|
d.state.OS.LxcPath,
|
|
configPath,
|
|
d.LogPath())
|
|
if err != nil && !d.IsRunning() {
|
|
// Attempt to extract the LXC errors
|
|
lxcLog := ""
|
|
logPath := filepath.Join(d.LogPath(), "lxc.log")
|
|
if util.PathExists(logPath) {
|
|
logContent, err := os.ReadFile(logPath)
|
|
if err == nil {
|
|
for _, line := range strings.Split(string(logContent), "\n") {
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 4 {
|
|
continue
|
|
}
|
|
|
|
// We only care about errors
|
|
if fields[2] != "ERROR" {
|
|
continue
|
|
}
|
|
|
|
// Prepend the line break
|
|
if len(lxcLog) == 0 {
|
|
lxcLog += "\n"
|
|
}
|
|
|
|
lxcLog += fmt.Sprintf(" %s\n", strings.Join(fields[0:], " "))
|
|
}
|
|
}
|
|
}
|
|
|
|
d.logger.Error("Failed starting instance", ctxMap)
|
|
|
|
// Return the actual error
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
// Run any post start hooks.
|
|
err = d.runHooks(postStartHooks)
|
|
if err != nil {
|
|
op.Done(err) // Must come before Stop() otherwise stop will not proceed.
|
|
|
|
// Attempt to stop container.
|
|
_ = d.Stop(false)
|
|
|
|
return err
|
|
}
|
|
|
|
// Apply OOM priority after container is started and hooks completed.
|
|
err = d.setOOMPriority(d.InitPID())
|
|
if err != nil {
|
|
d.logger.Warn("Failed to set OOM priority", logger.Ctx{
|
|
"err": err,
|
|
"instance": d.Name(),
|
|
"project": d.Project().Name,
|
|
})
|
|
}
|
|
|
|
if op.Action() == "start" {
|
|
d.logger.Info("Started instance", ctxMap)
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceStarted.Event(d, nil))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// OnHook is the top-level hook handler.
|
|
func (d *lxc) OnHook(hookName string, args map[string]string) error {
|
|
switch hookName {
|
|
case instance.HookStart:
|
|
return d.onStart(args)
|
|
case instance.HookStopNS:
|
|
return d.onStopNS(args)
|
|
case instance.HookStop:
|
|
return d.onStop(args)
|
|
default:
|
|
return instance.ErrNotImplemented
|
|
}
|
|
}
|
|
|
|
// onStart implements the start hook.
|
|
func (d *lxc) onStart(_ map[string]string) error {
|
|
// Make sure we can't call go-lxc functions by mistake
|
|
d.fromHook = true
|
|
|
|
// Load the container AppArmor profile
|
|
err := apparmor.InstanceLoad(d.state.OS, d, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Template anything that needs templating
|
|
key := "volatile.apply_template"
|
|
if d.localConfig[key] != "" {
|
|
// Run any template that needs running
|
|
err = d.templateApplyNow(instance.TemplateTrigger(d.localConfig[key]))
|
|
if err != nil {
|
|
_ = apparmor.InstanceUnload(d.state.OS, d)
|
|
return err
|
|
}
|
|
|
|
err := d.state.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
|
|
// Remove the volatile key from the DB
|
|
return tx.DeleteInstanceConfigKey(ctx, int64(d.id), key)
|
|
})
|
|
if err != nil {
|
|
_ = apparmor.InstanceUnload(d.state.OS, d)
|
|
return err
|
|
}
|
|
}
|
|
|
|
err = d.templateApplyNow("start")
|
|
if err != nil {
|
|
_ = apparmor.InstanceUnload(d.state.OS, d)
|
|
return err
|
|
}
|
|
|
|
// Trigger a rebalance
|
|
defer cgroup.TaskSchedulerTrigger("container", d.name, "started")
|
|
|
|
// Record last start state.
|
|
err = d.recordLastState()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateStartup checks any constraints that would prevent start up from succeeding under normal circumstances.
|
|
func (d *lxc) validateStartup(stateful bool, statusCode api.StatusCode) error {
|
|
err := d.common.validateStartup(stateful, statusCode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// gendoc:generate(entity=image, group=requirements, key=requirements.nesting)
|
|
//
|
|
// ---
|
|
// type: bool
|
|
// shortdesc: If set to `true`, indicates that the image cannot work without nesting enabled.
|
|
//
|
|
// Ensure nesting is turned on for images that require nesting.
|
|
if util.IsTrue(d.localConfig["image.requirements.nesting"]) && util.IsFalseOrEmpty(d.expandedConfig["security.nesting"]) {
|
|
return errors.New("The image used by this instance requires nesting. Please set security.nesting=true on the instance")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop functions.
|
|
func (d *lxc) Stop(stateful bool) error {
|
|
d.logger.Debug("Stop started", logger.Ctx{"stateful": stateful})
|
|
defer d.logger.Debug("Stop finished", logger.Ctx{"stateful": stateful})
|
|
|
|
// Check that migration.stateful is set for stateful actions.
|
|
if stateful && !d.CanLiveMigrate() {
|
|
return errors.New("Stateful stop requires the instance to have migration.stateful be set to true")
|
|
}
|
|
|
|
// Must be run prior to creating the operation lock.
|
|
if !d.IsRunning() {
|
|
return ErrInstanceIsStopped
|
|
}
|
|
|
|
// Setup a new operation
|
|
op, err := operationlock.CreateWaitGet(d.Project().Name, d.Name(), d.op, operationlock.ActionStop, []operationlock.Action{operationlock.ActionRestart, operationlock.ActionRestore, operationlock.ActionMigrate}, false, true)
|
|
if err != nil {
|
|
if errors.Is(err, operationlock.ErrNonReusuableSucceeded) {
|
|
// An existing matching operation has now succeeded, return.
|
|
return nil
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
ctxMap := logger.Ctx{
|
|
"action": op.Action(),
|
|
"created": d.creationDate,
|
|
"ephemeral": d.ephemeral,
|
|
"used": d.lastUsedDate,
|
|
"stateful": stateful,
|
|
}
|
|
|
|
if op.Action() == "stop" {
|
|
d.logger.Info("Stopping instance", ctxMap)
|
|
}
|
|
|
|
// Forcefully stop any forkfile process if running.
|
|
d.stopForkfile(true)
|
|
|
|
// Release liblxc container once done.
|
|
defer func() {
|
|
d.release()
|
|
}()
|
|
|
|
// Load the go-lxc struct
|
|
var cc *liblxc.Container
|
|
if d.expandedConfig["raw.lxc"] != "" {
|
|
cc, err = d.initLXC(true)
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
err = d.loadRawLXCConfig(cc)
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
} else {
|
|
cc, err = d.initLXC(false)
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Handle stateful stop
|
|
if stateful {
|
|
// Cleanup any existing state
|
|
stateDir := d.StatePath()
|
|
_ = os.RemoveAll(stateDir)
|
|
|
|
err := os.MkdirAll(stateDir, 0o700)
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
criuMigrationArgs := instance.CriuMigrationArgs{
|
|
Cmd: liblxc.MIGRATE_DUMP,
|
|
StateDir: stateDir,
|
|
Function: "snapshot",
|
|
Stop: true,
|
|
ActionScript: false,
|
|
DumpDir: "",
|
|
PreDumpDir: "",
|
|
}
|
|
|
|
// Checkpoint
|
|
err = d.migrate(&criuMigrationArgs)
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
err = op.Wait(context.Background())
|
|
if err != nil && d.IsRunning() {
|
|
return err
|
|
}
|
|
|
|
d.stateful = true
|
|
|
|
err = d.state.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
|
|
return tx.UpdateInstanceStatefulFlag(ctx, d.id, true)
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("Failed updating instance stateful flag: %w", err)
|
|
}
|
|
|
|
d.logger.Info("Stopped instance", ctxMap)
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceStopped.Event(d, nil))
|
|
|
|
return nil
|
|
} else if util.PathExists(d.StatePath()) {
|
|
_ = os.RemoveAll(d.StatePath())
|
|
}
|
|
|
|
// Load cgroup abstraction
|
|
cg, err := d.cgroup(cc, true)
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
// Fork-bomb mitigation, prevent forking from this point on
|
|
if d.state.OS.CGInfo.Supports(cgroup.Pids, cg) {
|
|
// Attempt to disable forking new processes
|
|
_ = cg.SetMaxProcesses(0)
|
|
} else if d.state.OS.CGInfo.Supports(cgroup.Freezer, cg) {
|
|
// Attempt to freeze the container
|
|
freezer := make(chan bool, 1)
|
|
go func() {
|
|
_ = d.Freeze()
|
|
freezer <- true
|
|
}()
|
|
|
|
select {
|
|
case <-freezer:
|
|
case <-time.After(time.Second * 5):
|
|
_ = d.Unfreeze()
|
|
}
|
|
}
|
|
|
|
err = cc.Stop()
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
// Wait for operation lock to be Done. This is normally completed by onStop which picks up the same
|
|
// operation lock and then marks it as Done after the instance stops and the devices have been cleaned up.
|
|
// However if the operation has failed for another reason we will collect the error here.
|
|
err = op.Wait(context.Background())
|
|
status := d.statusCode()
|
|
if status != api.Stopped {
|
|
errPrefix := fmt.Errorf("Failed stopping instance, status is %q", status)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", errPrefix.Error(), err)
|
|
}
|
|
|
|
return errPrefix
|
|
} else if op.Action() == "stop" {
|
|
// If instance stopped, send lifecycle event (even if there has been an error cleaning up).
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceStopped.Event(d, nil))
|
|
}
|
|
|
|
// Now handle errors from stop sequence and return to caller if wasn't completed cleanly.
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Shutdown stops the instance.
|
|
func (d *lxc) Shutdown(timeout time.Duration) error {
|
|
d.logger.Debug("Shutdown started", logger.Ctx{"timeout": timeout})
|
|
defer d.logger.Debug("Shutdown finished", logger.Ctx{"timeout": timeout})
|
|
|
|
// Must be run prior to creating the operation lock.
|
|
statusCode := d.statusCode()
|
|
if !d.isRunningStatusCode(statusCode) {
|
|
if statusCode == api.Error {
|
|
return fmt.Errorf("The instance cannot be cleanly shutdown as in %s status", statusCode)
|
|
}
|
|
|
|
return ErrInstanceIsStopped
|
|
}
|
|
|
|
// Setup a new operation
|
|
op, err := operationlock.CreateWaitGet(d.Project().Name, d.Name(), d.op, operationlock.ActionStop, []operationlock.Action{operationlock.ActionRestart}, true, true)
|
|
if err != nil {
|
|
if errors.Is(err, operationlock.ErrNonReusuableSucceeded) {
|
|
// An existing matching operation has now succeeded, return.
|
|
return nil
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// If frozen, resume so the signal can be handled.
|
|
if d.IsFrozen() {
|
|
err := d.Unfreeze()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
ctxMap := logger.Ctx{
|
|
"action": "shutdown",
|
|
"created": d.creationDate,
|
|
"ephemeral": d.ephemeral,
|
|
"used": d.lastUsedDate,
|
|
"timeout": timeout,
|
|
}
|
|
|
|
if op.Action() == "stop" {
|
|
d.logger.Info("Shutting down instance", ctxMap)
|
|
}
|
|
|
|
// Release liblxc container once done.
|
|
defer func() {
|
|
d.release()
|
|
}()
|
|
|
|
// Load the go-lxc struct
|
|
var cc *liblxc.Container
|
|
if d.expandedConfig["raw.lxc"] != "" {
|
|
cc, err = d.initLXC(true)
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
err = d.loadRawLXCConfig(cc)
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
} else {
|
|
cc, err = d.initLXC(false)
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Request shutdown, but don't wait for container to stop. If call fails then cancel operation with error,
|
|
// otherwise expect the onStop() hook to cancel operation when done (when the container has stopped).
|
|
err = cc.Shutdown(0)
|
|
if err != nil {
|
|
op.Done(err)
|
|
}
|
|
|
|
d.logger.Debug("Shutdown request sent to instance")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
|
|
// Wait for operation lock to be Done or context to timeout. The operation lock is normally completed by
|
|
// onStop which picks up the same lock and then marks it as Done after the instance stops and the devices
|
|
// have been cleaned up. However if the operation has failed for another reason we collect the error here.
|
|
err = op.Wait(ctx)
|
|
status := d.statusCode()
|
|
if status != api.Stopped {
|
|
errPrefix := fmt.Errorf("Failed shutting down instance, status is %q", status)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", errPrefix.Error(), err)
|
|
}
|
|
|
|
return errPrefix
|
|
} else if op.Action() == "stop" {
|
|
// If instance stopped, send lifecycle event (even if there has been an error cleaning up).
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceShutdown.Event(d, nil))
|
|
}
|
|
|
|
// Now handle errors from shutdown sequence and return to caller if wasn't completed cleanly.
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Restart restart the instance.
|
|
func (d *lxc) Restart(timeout time.Duration) error {
|
|
return d.restartCommon(d, timeout)
|
|
}
|
|
|
|
// Rebuild rebuilds the instance using the supplied image fingerprint as source.
|
|
func (d *lxc) Rebuild(img *api.Image, op *operations.Operation) error {
|
|
return d.rebuildCommon(d, img, op)
|
|
}
|
|
|
|
// onStopNS is triggered by LXC's stop hook once a container is shutdown but before the container's
|
|
// namespaces have been closed. The netns path of the stopped container is provided.
|
|
func (d *lxc) onStopNS(args map[string]string) error {
|
|
target := args["target"]
|
|
netns := args["netns"]
|
|
|
|
// Validate target.
|
|
if !slices.Contains([]string{"stop", "reboot"}, target) {
|
|
d.logger.Error("Container sent invalid target to OnStopNS", logger.Ctx{"target": target})
|
|
return fmt.Errorf("Invalid stop target %q", target)
|
|
}
|
|
|
|
// Create/pick up operation, but don't complete it as we leave operation running for the onStop hook below.
|
|
_, err := d.onStopOperationSetup(target)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Clean up devices.
|
|
d.cleanupDevices(false, netns)
|
|
|
|
return nil
|
|
}
|
|
|
|
// onStop is triggered by LXC's post-stop hook once a container is shutdown and after the
|
|
// container's namespaces have been closed.
|
|
func (d *lxc) onStop(args map[string]string) error {
|
|
target := args["target"]
|
|
|
|
// Validate target
|
|
if !slices.Contains([]string{"stop", "reboot"}, target) {
|
|
d.logger.Error("Container sent invalid target to OnStop", logger.Ctx{"target": target})
|
|
return fmt.Errorf("Invalid stop target: %s", target)
|
|
}
|
|
|
|
// Create/pick up operation.
|
|
op, err := d.onStopOperationSetup(target)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Make sure we can't call go-lxc functions by mistake
|
|
d.fromHook = true
|
|
|
|
// Record power state.
|
|
err = d.VolatileSet(map[string]string{
|
|
"volatile.last_state.power": instance.PowerStateStopped,
|
|
"volatile.last_state.ready": "false",
|
|
})
|
|
if err != nil {
|
|
// Don't return an error here as we still want to cleanup the instance even if DB not available.
|
|
d.logger.Error("Failed recording last power state", logger.Ctx{"err": err})
|
|
}
|
|
|
|
go func(d *lxc, target string, op *operationlock.InstanceOperation) {
|
|
d.fromHook = false
|
|
err = nil
|
|
|
|
// Set operation if missing.
|
|
if d.op == nil {
|
|
d.op = op.GetOperation()
|
|
}
|
|
|
|
// Unlock on return
|
|
defer op.Done(nil)
|
|
|
|
d.logger.Debug("Instance stopped, cleaning up")
|
|
|
|
// Wait for any file operations to complete.
|
|
// This is to required so we can actually unmount the container.
|
|
d.stopForkfile(false)
|
|
|
|
// Clean up devices.
|
|
d.cleanupDevices(false, "")
|
|
|
|
// Remove directory ownership (to avoid issue if uidmap is reused)
|
|
err := os.Chown(d.Path(), 0, 0)
|
|
if err != nil {
|
|
op.Done(fmt.Errorf("Failed clearing ownership: %w", err))
|
|
return
|
|
}
|
|
|
|
err = os.Chmod(d.Path(), 0o100)
|
|
if err != nil {
|
|
op.Done(fmt.Errorf("Failed clearing permissions: %w", err))
|
|
return
|
|
}
|
|
|
|
// Stop the storage for this container
|
|
err = d.unmount()
|
|
if err != nil && !errors.Is(err, storageDrivers.ErrInUse) {
|
|
err = fmt.Errorf("Failed unmounting instance: %w", err)
|
|
op.Done(err)
|
|
return
|
|
}
|
|
|
|
// Unload the apparmor profile
|
|
err = apparmor.InstanceUnload(d.state.OS, d)
|
|
if err != nil {
|
|
op.Done(fmt.Errorf("Failed to destroy apparmor namespace: %w", err))
|
|
return
|
|
}
|
|
|
|
// Clean all the unix devices
|
|
err = d.removeUnixDevices()
|
|
if err != nil {
|
|
op.Done(fmt.Errorf("Failed to remove unix devices: %w", err))
|
|
return
|
|
}
|
|
|
|
// Clean all the disk devices
|
|
err = d.removeDiskDevices()
|
|
if err != nil {
|
|
op.Done(fmt.Errorf("Failed to remove disk devices: %w", err))
|
|
return
|
|
}
|
|
|
|
// Stop dedicated LXCFS.
|
|
if util.PathExists(filepath.Join(d.DevicesPath(), "lxcfs", "proc")) && util.PathExists(filepath.Join(d.RunPath(), "lxcfs.yaml")) {
|
|
// Import the running LXCFS.
|
|
lxcfs, err := subprocess.ImportProcess(filepath.Join(d.RunPath(), "lxcfs.yaml"))
|
|
if err != nil && !os.IsExist(err) {
|
|
op.Done(fmt.Errorf("Failed to stop LXCFS: %w", err))
|
|
return
|
|
}
|
|
|
|
// Stop LXCFS.
|
|
err = lxcfs.Stop()
|
|
if err != nil && !errors.Is(err, subprocess.ErrNotRunning) {
|
|
op.Done(fmt.Errorf("Failed to stop LXCFS: %w", err))
|
|
return
|
|
}
|
|
|
|
_ = unix.Unmount(filepath.Join(d.DevicesPath(), "lxcfs"), unix.MNT_DETACH)
|
|
}
|
|
|
|
// Determine if instance should be auto-restarted.
|
|
var autoRestart bool
|
|
if target != "reboot" && op.GetInstanceInitiated() && d.shouldAutoRestart() {
|
|
autoRestart = true
|
|
|
|
// Mark current shutdown as complete.
|
|
op.Done(nil)
|
|
|
|
// Create a new restart operation.
|
|
op, err = operationlock.CreateWaitGet(d.Project().Name, d.Name(), d.op, operationlock.ActionRestart, nil, true, false)
|
|
if err == nil {
|
|
defer op.Done(nil)
|
|
} else {
|
|
d.logger.Error("Failed to setup new restart operation", logger.Ctx{"err": err})
|
|
}
|
|
}
|
|
|
|
// Log and emit lifecycle if not user triggered
|
|
if target != "reboot" && !autoRestart && op.GetInstanceInitiated() {
|
|
ctxMap := logger.Ctx{
|
|
"action": target,
|
|
"created": d.creationDate,
|
|
"ephemeral": d.ephemeral,
|
|
"used": d.lastUsedDate,
|
|
"stateful": false,
|
|
}
|
|
|
|
d.logger.Info("Shut down instance", ctxMap)
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceShutdown.Event(d, nil))
|
|
}
|
|
|
|
// Reboot the container
|
|
if target == "reboot" || autoRestart {
|
|
// Start the container again
|
|
err = d.Start(false)
|
|
if err != nil {
|
|
op.Done(fmt.Errorf("Failed restarting instance: %w", err))
|
|
return
|
|
}
|
|
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceRestarted.Event(d, nil))
|
|
|
|
return
|
|
}
|
|
|
|
// Trigger a rebalance
|
|
defer cgroup.TaskSchedulerTrigger("container", d.name, "stopped")
|
|
|
|
// Destroy ephemeral containers
|
|
if d.ephemeral {
|
|
err = d.delete(true)
|
|
if err != nil {
|
|
op.Done(fmt.Errorf("Failed deleting ephemeral instance: %w", err))
|
|
return
|
|
}
|
|
}
|
|
}(d, target, op)
|
|
|
|
return nil
|
|
}
|
|
|
|
// cleanupDevices performs any needed device cleanup steps when container is stopped.
|
|
// Accepts a stopHookNetnsPath argument which is required when run from the onStopNS hook before the
|
|
// container's network namespace is unmounted (which is required for NIC device cleanup).
|
|
func (d *lxc) cleanupDevices(instanceRunning bool, stopHookNetnsPath string) {
|
|
for _, entry := range d.expandedDevices.Reversed() {
|
|
// Only stop NIC devices when run from the onStopNS hook, and stop all other devices when run from
|
|
// the onStop hook. This way disk devices are stopped after the instance has been fully stopped.
|
|
if (stopHookNetnsPath != "" && entry.Config["type"] != "nic") || (stopHookNetnsPath == "" && entry.Config["type"] == "nic") {
|
|
continue
|
|
}
|
|
|
|
dev, err := d.deviceLoad(d, entry.Name, entry.Config)
|
|
if err != nil {
|
|
if errors.Is(err, device.ErrUnsupportedDevType) {
|
|
continue // Skip unsupported device (allows for mixed instance type profiles).
|
|
}
|
|
|
|
// Just log an error, but still allow the device to be stopped if usable device returned.
|
|
d.logger.Error("Failed stop validation for device", logger.Ctx{"device": entry.Name, "err": err})
|
|
}
|
|
|
|
// If a usable device was returned from deviceLoad try to stop anyway, even if validation fails.
|
|
// This allows for the scenario where a new version has additional validation restrictions
|
|
// than older versions and we still need to allow previously valid devices to be stopped even if
|
|
// they are no longer considered valid.
|
|
if dev != nil {
|
|
err = d.deviceStop(dev, instanceRunning, stopHookNetnsPath)
|
|
if err != nil {
|
|
d.logger.Error("Failed to stop device", logger.Ctx{"device": dev.Name(), "err": err})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Freeze functions.
|
|
func (d *lxc) Freeze() error {
|
|
ctxMap := logger.Ctx{
|
|
"created": d.creationDate,
|
|
"ephemeral": d.ephemeral,
|
|
"used": d.lastUsedDate,
|
|
}
|
|
|
|
// Check that we're running
|
|
if !d.IsRunning() {
|
|
return errors.New("The instance isn't running")
|
|
}
|
|
|
|
// Load the go-lxc struct
|
|
cc, err := d.initLXC(false)
|
|
if err != nil {
|
|
ctxMap["err"] = err
|
|
d.logger.Error("Failed freezing container", ctxMap)
|
|
return err
|
|
}
|
|
|
|
cg, err := d.cgroup(cc, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check if the CGroup is available
|
|
if !d.state.OS.CGInfo.Supports(cgroup.Freezer, cg) {
|
|
d.logger.Warn("Unable to freeze container (lack of kernel support)", ctxMap)
|
|
return nil
|
|
}
|
|
|
|
// Check that we're not already frozen
|
|
if d.IsFrozen() {
|
|
return errors.New("The container is already frozen")
|
|
}
|
|
|
|
d.logger.Info("Freezing container", ctxMap)
|
|
|
|
err = cc.Freeze()
|
|
if err != nil {
|
|
ctxMap["err"] = err
|
|
d.logger.Error("Failed freezing container", ctxMap)
|
|
return err
|
|
}
|
|
|
|
d.logger.Info("Froze container", ctxMap)
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstancePaused.Event(d, nil))
|
|
|
|
return err
|
|
}
|
|
|
|
// Unfreeze unfreezes the instance.
|
|
func (d *lxc) Unfreeze() error {
|
|
ctxMap := logger.Ctx{
|
|
"created": d.creationDate,
|
|
"ephemeral": d.ephemeral,
|
|
"used": d.lastUsedDate,
|
|
}
|
|
|
|
// Check that we're running
|
|
if !d.IsRunning() {
|
|
return errors.New("The container isn't running")
|
|
}
|
|
|
|
// Load the go-lxc struct
|
|
cc, err := d.initLXC(false)
|
|
if err != nil {
|
|
d.logger.Error("Failed unfreezing container", ctxMap)
|
|
return err
|
|
}
|
|
|
|
cg, err := d.cgroup(cc, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check if the CGroup is available
|
|
if !d.state.OS.CGInfo.Supports(cgroup.Freezer, cg) {
|
|
d.logger.Warn("Unable to unfreeze container (lack of kernel support)", ctxMap)
|
|
return nil
|
|
}
|
|
|
|
// Check that we're frozen
|
|
if !d.IsFrozen() {
|
|
return errors.New("The container is already running")
|
|
}
|
|
|
|
d.logger.Info("Unfreezing container", ctxMap)
|
|
|
|
err = cc.Unfreeze()
|
|
if err != nil {
|
|
d.logger.Error("Failed unfreezing container", ctxMap)
|
|
}
|
|
|
|
d.logger.Info("Unfroze container", ctxMap)
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceResumed.Event(d, nil))
|
|
|
|
return err
|
|
}
|
|
|
|
// Get lxc container state, with 1 second timeout.
|
|
// If we don't get a reply, assume the lxc monitor is unresponsive.
|
|
func (d *lxc) getLxcState() (liblxc.State, error) {
|
|
if d.IsSnapshot() {
|
|
return liblxc.StateMap["STOPPED"], nil
|
|
}
|
|
|
|
// Load the go-lxc struct
|
|
cc, err := d.initLXC(false)
|
|
if err != nil {
|
|
return liblxc.StateMap["STOPPED"], err
|
|
}
|
|
|
|
monitor := make(chan liblxc.State, 1)
|
|
|
|
go func(c *liblxc.Container) {
|
|
monitor <- c.State()
|
|
}(cc)
|
|
|
|
select {
|
|
case state := <-monitor:
|
|
return state, nil
|
|
case <-time.After(5 * time.Second):
|
|
return liblxc.StateMap["FROZEN"], errors.New("Monitor is unresponsive")
|
|
}
|
|
}
|
|
|
|
// RenderWithUsage renders the API response including disk usage.
|
|
func (d *lxc) RenderWithUsage() (any, any, error) {
|
|
resp, etag, err := d.Render()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Currently only snapshot data needs usage added.
|
|
snapResp, ok := resp.(*api.InstanceSnapshot)
|
|
if !ok {
|
|
return resp, etag, nil
|
|
}
|
|
|
|
pool, err := d.getStoragePool()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// It is important that the snapshot not be mounted here as mounting a snapshot can trigger a very
|
|
// expensive filesystem UUID regeneration, so we rely on the driver implementation to get the info
|
|
// we are requesting as cheaply as possible.
|
|
volumeState, err := pool.GetInstanceUsage(d)
|
|
if err != nil {
|
|
return resp, etag, nil
|
|
}
|
|
|
|
snapResp.Size = volumeState.Used
|
|
return snapResp, etag, nil
|
|
}
|
|
|
|
// Render renders the state of the instance.
|
|
func (d *lxc) Render() (any, any, error) {
|
|
// Ignore err as the arch string on error is correct (unknown)
|
|
architectureName, _ := osarch.ArchitectureName(d.architecture)
|
|
profileNames := make([]string, 0, len(d.profiles))
|
|
for _, profile := range d.profiles {
|
|
profileNames = append(profileNames, profile.Name)
|
|
}
|
|
|
|
if d.IsSnapshot() {
|
|
// Prepare the response.
|
|
snapState := api.InstanceSnapshot{
|
|
CreatedAt: d.creationDate,
|
|
Description: d.description,
|
|
ExpandedConfig: d.expandedConfig,
|
|
ExpandedDevices: d.expandedDevices.CloneNative(),
|
|
LastUsedAt: d.lastUsedDate,
|
|
Name: strings.SplitN(d.name, "/", 2)[1],
|
|
Stateful: d.stateful,
|
|
Size: -1, // Default to uninitialized/error state (0 means no CoW usage).
|
|
}
|
|
|
|
snapState.Architecture = architectureName
|
|
snapState.Config = d.localConfig
|
|
snapState.Devices = d.localDevices.CloneNative()
|
|
snapState.Ephemeral = d.ephemeral
|
|
snapState.Profiles = profileNames
|
|
snapState.ExpiresAt = d.expiryDate
|
|
|
|
return &snapState, d.ETag(), nil
|
|
}
|
|
|
|
// Prepare the response.
|
|
statusCode := d.statusCode()
|
|
instState := api.Instance{
|
|
ExpandedConfig: d.expandedConfig,
|
|
ExpandedDevices: d.expandedDevices.CloneNative(),
|
|
Name: d.name,
|
|
Status: statusCode.String(),
|
|
StatusCode: statusCode,
|
|
Location: d.node,
|
|
Type: d.Type().String(),
|
|
}
|
|
|
|
instState.Description = d.description
|
|
instState.Architecture = architectureName
|
|
instState.Config = d.localConfig
|
|
instState.CreatedAt = d.creationDate
|
|
instState.Devices = d.localDevices.CloneNative()
|
|
instState.Ephemeral = d.ephemeral
|
|
instState.LastUsedAt = d.lastUsedDate
|
|
instState.Profiles = profileNames
|
|
instState.Stateful = d.stateful
|
|
instState.Project = d.project.Name
|
|
|
|
return &instState, d.ETag(), nil
|
|
}
|
|
|
|
// RenderFull renders the full state of the instance.
|
|
func (d *lxc) RenderFull(hostInterfaces []net.Interface) (*api.InstanceFull, any, error) {
|
|
if d.IsSnapshot() {
|
|
return nil, nil, errors.New("RenderFull only works with containers")
|
|
}
|
|
|
|
// Pre-fetch the data.
|
|
pool, err := d.getStoragePool()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
err = pool.CacheInstanceSnapshots(d)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Get the Container struct
|
|
base, etag, err := d.Render()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Convert to ContainerFull
|
|
ct := api.InstanceFull{Instance: *base.(*api.Instance)}
|
|
|
|
// Add the ContainerState
|
|
ct.State, err = d.renderState(ct.StatusCode, hostInterfaces)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Add the ContainerSnapshots
|
|
snaps, err := d.Snapshots()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
for _, snap := range snaps {
|
|
render, _, err := snap.Render()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if ct.Snapshots == nil {
|
|
ct.Snapshots = []api.InstanceSnapshot{}
|
|
}
|
|
|
|
ct.Snapshots = append(ct.Snapshots, *render.(*api.InstanceSnapshot))
|
|
}
|
|
|
|
// Add the ContainerBackups
|
|
backups, err := d.Backups()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
for _, backup := range backups {
|
|
render := backup.Render()
|
|
|
|
if ct.Backups == nil {
|
|
ct.Backups = []api.InstanceBackup{}
|
|
}
|
|
|
|
ct.Backups = append(ct.Backups, *render)
|
|
}
|
|
|
|
return &ct, etag, nil
|
|
}
|
|
|
|
// renderState renders just the running state of the instance.
|
|
func (d *lxc) renderState(statusCode api.StatusCode, hostInterfaces []net.Interface) (*api.InstanceState, error) {
|
|
status := api.InstanceState{
|
|
Status: statusCode.String(),
|
|
StatusCode: statusCode,
|
|
}
|
|
|
|
pid := d.InitPID()
|
|
processesState, _ := d.processesState(pid)
|
|
|
|
if d.isRunningStatusCode(statusCode) {
|
|
var err error
|
|
|
|
status.CPU = d.cpuState()
|
|
status.Memory = d.memoryState()
|
|
status.Network = d.networkState(hostInterfaces)
|
|
status.Pid = int64(pid)
|
|
status.Processes = processesState
|
|
|
|
status.StartedAt, err = d.processStartedAt(d.InitPID())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
status.Disk = d.diskState()
|
|
|
|
d.release()
|
|
|
|
return &status, nil
|
|
}
|
|
|
|
// RenderState renders just the running state of the instance.
|
|
func (d *lxc) RenderState(hostInterfaces []net.Interface) (*api.InstanceState, error) {
|
|
return d.renderState(d.statusCode(), hostInterfaces)
|
|
}
|
|
|
|
// snapshot creates a snapshot of the instance.
|
|
func (d *lxc) snapshot(name string, expiry time.Time, stateful bool) error {
|
|
// Check that migration.stateful is set for stateful actions.
|
|
if stateful && !d.CanLiveMigrate() {
|
|
return errors.New("Stateful snapshots require that the instance has migration.stateful be set to true")
|
|
}
|
|
|
|
// Deal with state.
|
|
if stateful {
|
|
// Quick checks.
|
|
if !d.IsRunning() {
|
|
return errors.New("Unable to create a stateful snapshot. The instance isn't running")
|
|
}
|
|
|
|
_, err := exec.LookPath("criu")
|
|
if err != nil {
|
|
return errors.New("Unable to create a stateful snapshot. CRIU isn't installed")
|
|
}
|
|
|
|
// Cleanup any existing state
|
|
stateDir := d.StatePath()
|
|
_ = os.RemoveAll(stateDir)
|
|
|
|
// Create the state path and make sure we don't keep state around after the snapshot has been made.
|
|
err = os.MkdirAll(stateDir, 0o700)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() { _ = os.RemoveAll(stateDir) }()
|
|
|
|
// Release liblxc container once done.
|
|
defer func() {
|
|
d.release()
|
|
}()
|
|
|
|
// Load the go-lxc struct
|
|
if d.expandedConfig["raw.lxc"] != "" {
|
|
cc, err := d.initLXC(true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = d.loadRawLXCConfig(cc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
_, err = d.initLXC(false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
/* TODO: ideally we would freeze here and unfreeze below after
|
|
* we've copied the filesystem, to make sure there are no
|
|
* changes by the container while snapshotting. Unfortunately
|
|
* there is abug in CRIU where it doesn't leave the container
|
|
* in the same state it found it w.r.t. freezing, i.e. CRIU
|
|
* freezes too, and then /always/ thaws, even if the container
|
|
* was frozen. Until that's fixed, all calls to Unfreeze()
|
|
* after snapshotting will fail.
|
|
*/
|
|
criuMigrationArgs := instance.CriuMigrationArgs{
|
|
Cmd: liblxc.MIGRATE_DUMP,
|
|
StateDir: stateDir,
|
|
Function: "snapshot",
|
|
Stop: false,
|
|
ActionScript: false,
|
|
DumpDir: "",
|
|
PreDumpDir: "",
|
|
}
|
|
|
|
// Dump the state.
|
|
err = d.migrate(&criuMigrationArgs)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed taking stateful checkpoint: %w", err)
|
|
}
|
|
}
|
|
|
|
// Wait for any file operations to complete to have a more consistent snapshot.
|
|
d.stopForkfile(false)
|
|
|
|
return d.snapshotCommon(d, name, expiry, stateful)
|
|
}
|
|
|
|
// Snapshot takes a new snapshot.
|
|
func (d *lxc) Snapshot(name string, expiry time.Time, stateful bool) error {
|
|
return d.snapshot(name, expiry, stateful)
|
|
}
|
|
|
|
// Restore restores a snapshot.
|
|
func (d *lxc) Restore(sourceContainer instance.Instance, stateful bool, diskOnly bool) error {
|
|
var ctxMap logger.Ctx
|
|
|
|
op, err := operationlock.Create(d.Project().Name, d.Name(), d.op, operationlock.ActionRestore, false, false)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to create instance restore operation: %w", err)
|
|
}
|
|
|
|
defer op.Done(nil)
|
|
|
|
// Stop the container.
|
|
wasRunning := d.IsRunning()
|
|
if wasRunning {
|
|
ephemeral := d.IsEphemeral()
|
|
if ephemeral {
|
|
// Unset ephemeral flag.
|
|
args := db.InstanceArgs{
|
|
Architecture: d.Architecture(),
|
|
Config: d.LocalConfig(),
|
|
Description: d.Description(),
|
|
Devices: d.LocalDevices(),
|
|
Ephemeral: false,
|
|
Profiles: d.Profiles(),
|
|
Project: d.Project().Name,
|
|
Type: d.Type(),
|
|
Snapshot: d.IsSnapshot(),
|
|
}
|
|
|
|
err := d.Update(args, false)
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
// On function return, set the flag back on.
|
|
defer func() {
|
|
args.Ephemeral = ephemeral
|
|
_ = d.Update(args, false)
|
|
}()
|
|
}
|
|
|
|
// This will unmount the container storage.
|
|
err := d.Stop(false)
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
// Refresh the operation as that one is now complete.
|
|
op, err = operationlock.Create(d.Project().Name, d.Name(), d.op, operationlock.ActionRestore, false, false)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to create instance restore operation: %w", err)
|
|
}
|
|
|
|
defer op.Done(nil)
|
|
}
|
|
|
|
ctxMap = logger.Ctx{
|
|
"created": d.creationDate,
|
|
"ephemeral": d.ephemeral,
|
|
"used": d.lastUsedDate,
|
|
"source": sourceContainer.Name(),
|
|
}
|
|
|
|
d.logger.Info("Restoring instance", ctxMap)
|
|
|
|
// Wait for any file operations to complete.
|
|
// This is required so we can actually unmount the container and restore its rootfs.
|
|
d.stopForkfile(false)
|
|
|
|
// Initialize storage interface for the container and mount the rootfs for criu state check.
|
|
pool, err := storagePools.LoadByInstance(d.state, d)
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
d.logger.Debug("Mounting instance to check for CRIU state path existence")
|
|
|
|
reverter := revert.New()
|
|
defer reverter.Fail()
|
|
|
|
// Ensure that storage is mounted for state path checks and for backup.yaml updates.
|
|
_, err = d.mount()
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
reverter.Add(func() { _ = d.unmount() })
|
|
|
|
// Check for CRIU if necessary, before doing a bunch of filesystem manipulations.
|
|
// Requires container be mounted to check StatePath exists.
|
|
if util.PathExists(d.StatePath()) {
|
|
_, err := exec.LookPath("criu")
|
|
if err != nil {
|
|
err = errors.New("Failed to restore container state. CRIU isn't installed")
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
err = d.unmount()
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
reverter.Success()
|
|
|
|
// Restore the rootfs.
|
|
err = pool.RestoreInstanceSnapshot(d, sourceContainer, nil)
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
args := db.InstanceArgs{}
|
|
if !diskOnly {
|
|
// Restore the configuration.
|
|
args = db.InstanceArgs{
|
|
Architecture: sourceContainer.Architecture(),
|
|
Config: sourceContainer.LocalConfig(),
|
|
Description: sourceContainer.Description(),
|
|
Devices: sourceContainer.LocalDevices(),
|
|
Ephemeral: sourceContainer.IsEphemeral(),
|
|
Profiles: sourceContainer.Profiles(),
|
|
Project: sourceContainer.Project().Name,
|
|
Type: sourceContainer.Type(),
|
|
Snapshot: sourceContainer.IsSnapshot(),
|
|
}
|
|
} else {
|
|
args = db.InstanceArgs{
|
|
Architecture: d.Architecture(),
|
|
Config: d.LocalConfig(),
|
|
Description: d.Description(),
|
|
Devices: d.LocalDevices(),
|
|
Ephemeral: d.IsEphemeral(),
|
|
Profiles: d.Profiles(),
|
|
Project: d.Project().Name,
|
|
Type: d.Type(),
|
|
Snapshot: d.IsSnapshot(),
|
|
}
|
|
|
|
args.Config["volatile.uuid.generation"] = sourceContainer.LocalConfig()["volatile.uuid.generation"]
|
|
}
|
|
|
|
// Don't pass as user-requested as there's no way to fix a bad config.
|
|
// This will call d.UpdateBackupFile() to ensure snapshot list is up to date.
|
|
err = d.Update(args, false)
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
// If the container wasn't running but was stateful, should we restore it as running?
|
|
if stateful {
|
|
if !util.PathExists(d.StatePath()) {
|
|
err = errors.New("Stateful snapshot restore requested but snapshot is stateless")
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
d.logger.Debug("Performing stateful restore", ctxMap)
|
|
d.stateful = true
|
|
|
|
criuMigrationArgs := instance.CriuMigrationArgs{
|
|
Cmd: liblxc.MIGRATE_RESTORE,
|
|
StateDir: d.StatePath(),
|
|
Function: "snapshot",
|
|
Stop: false,
|
|
ActionScript: false,
|
|
DumpDir: "",
|
|
PreDumpDir: "",
|
|
}
|
|
|
|
// Checkpoint.
|
|
err = d.migrate(&criuMigrationArgs)
|
|
if err != nil {
|
|
op.Done(err)
|
|
return fmt.Errorf("Failed taking stateful checkpoint: %w", err)
|
|
}
|
|
|
|
// Remove the state from the parent container; we only keep this in snapshots.
|
|
err2 := os.RemoveAll(d.StatePath())
|
|
if err2 != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
d.logger.Debug("Performed stateful restore", ctxMap)
|
|
d.logger.Info("Restored instance", ctxMap)
|
|
return nil
|
|
}
|
|
|
|
// Restart the container.
|
|
if wasRunning {
|
|
d.logger.Debug("Starting instance after snapshot restore")
|
|
err = d.Start(false)
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceRestored.Event(d, map[string]any{"snapshot": sourceContainer.Name()}))
|
|
d.logger.Info("Restored instance", ctxMap)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *lxc) cleanup() {
|
|
// Unmount any leftovers
|
|
_ = d.removeUnixDevices()
|
|
_ = d.removeDiskDevices()
|
|
|
|
// Remove the security profiles
|
|
_ = apparmor.InstanceDelete(d.state.OS, d)
|
|
seccomp.DeleteProfile(d)
|
|
|
|
// Remove the devices path
|
|
_ = os.Remove(d.DevicesPath())
|
|
|
|
// Remove the shmounts path
|
|
_ = os.RemoveAll(d.ShmountsPath())
|
|
}
|
|
|
|
// Delete deletes the instance.
|
|
func (d *lxc) Delete(force bool) error {
|
|
// Setup a new operation.
|
|
op, err := operationlock.CreateWaitGet(d.Project().Name, d.Name(), d.op, operationlock.ActionDelete, nil, false, false)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to create instance delete operation: %w", err)
|
|
}
|
|
|
|
defer op.Done(nil)
|
|
|
|
if d.IsRunning() {
|
|
return api.StatusErrorf(http.StatusBadRequest, "Instance is running")
|
|
}
|
|
|
|
err = d.delete(force)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If dealing with a snapshot, refresh the backup file on the parent.
|
|
if d.IsSnapshot() {
|
|
parentName, _, _ := api.GetParentAndSnapshotName(d.name)
|
|
|
|
// Load the parent.
|
|
parent, err := instance.LoadByProjectAndName(d.state, d.project.Name, parentName)
|
|
if err != nil {
|
|
return fmt.Errorf("Invalid parent: %w", err)
|
|
}
|
|
|
|
// Update the backup file.
|
|
err = parent.UpdateBackupFile()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Delete deletes the instance without creating an operation lock.
|
|
func (d *lxc) delete(force bool) error {
|
|
ctxMap := logger.Ctx{
|
|
"created": d.creationDate,
|
|
"ephemeral": d.ephemeral,
|
|
"used": d.lastUsedDate,
|
|
}
|
|
|
|
if d.isSnapshot {
|
|
d.logger.Info("Deleting instance snapshot", ctxMap)
|
|
} else {
|
|
d.logger.Info("Deleting instance", ctxMap)
|
|
}
|
|
|
|
if !force && util.IsTrue(d.expandedConfig["security.protection.delete"]) && !d.IsSnapshot() {
|
|
err := errors.New("Instance is protected")
|
|
d.logger.Warn("Failed to delete instance", logger.Ctx{"err": err})
|
|
return err
|
|
}
|
|
|
|
// Wait for any file operations to complete.
|
|
// This is required so we can actually unmount the container and delete it.
|
|
if !d.IsSnapshot() {
|
|
d.stopForkfile(false)
|
|
}
|
|
|
|
// Delete any persistent warnings for instance.
|
|
err := d.warningsDelete()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pool, err := storagePools.LoadByInstance(d.state, d)
|
|
if err != nil && !response.IsNotFoundError(err) {
|
|
return err
|
|
} else if pool != nil {
|
|
if d.IsSnapshot() {
|
|
// Remove snapshot volume and database record.
|
|
err = pool.DeleteInstanceSnapshot(d, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// Remove all snapshots.
|
|
err := d.deleteSnapshots(func(snapInst instance.Instance) error {
|
|
return snapInst.(*lxc).delete(true) // Internal delete function that doesn't lock.
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("Failed deleting instance snapshots: %w", err)
|
|
}
|
|
|
|
// Remove the storage volume and database records.
|
|
err = pool.DeleteInstance(d, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Perform other cleanup steps if not snapshot.
|
|
if !d.IsSnapshot() {
|
|
// Remove all backups.
|
|
backups, err := d.Backups()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, backup := range backups {
|
|
err = backup.Delete()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Run device removal function for each device.
|
|
d.devicesRemove(d)
|
|
|
|
// Clean things up.
|
|
d.cleanup()
|
|
}
|
|
|
|
err = d.state.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
|
|
// Remove the database record of the instance or snapshot instance.
|
|
return tx.DeleteInstance(ctx, d.project.Name, d.Name())
|
|
})
|
|
if err != nil {
|
|
d.logger.Error("Failed deleting instance entry", logger.Ctx{"err": err})
|
|
return err
|
|
}
|
|
|
|
if d.isSnapshot {
|
|
d.logger.Info("Deleted instance snapshot", ctxMap)
|
|
} else {
|
|
d.logger.Info("Deleted instance", ctxMap)
|
|
}
|
|
|
|
if d.isSnapshot {
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceSnapshotDeleted.Event(d, nil))
|
|
} else {
|
|
err = d.state.Authorizer.DeleteInstance(d.state.ShutdownCtx, d.project.Name, d.Name())
|
|
if err != nil {
|
|
logger.Error("Failed to remove instance from authorizer", logger.Ctx{"name": d.Name(), "project": d.project.Name, "error": err})
|
|
}
|
|
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceDeleted.Event(d, nil))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Rename renames the instance. Accepts an argument to enable applying deferred TemplateTriggerRename.
|
|
func (d *lxc) Rename(newName string, applyTemplateTrigger bool) error {
|
|
oldName := d.Name()
|
|
ctxMap := logger.Ctx{
|
|
"created": d.creationDate,
|
|
"ephemeral": d.ephemeral,
|
|
"used": d.lastUsedDate,
|
|
"newname": newName,
|
|
}
|
|
|
|
d.logger.Info("Renaming instance", ctxMap)
|
|
|
|
// Quick checks.
|
|
err := instance.ValidName(newName, d.IsSnapshot())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if d.IsRunning() {
|
|
return errors.New("Renaming of running instance not allowed")
|
|
}
|
|
|
|
// Clean things up.
|
|
d.cleanup()
|
|
|
|
pool, err := storagePools.LoadByInstance(d.state, d)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed loading instance storage pool: %w", err)
|
|
}
|
|
|
|
if d.IsSnapshot() {
|
|
_, newSnapName, _ := api.GetParentAndSnapshotName(newName)
|
|
err = pool.RenameInstanceSnapshot(d, newSnapName, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("Rename instance snapshot: %w", err)
|
|
}
|
|
} else {
|
|
err = pool.RenameInstance(d, newName, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("Rename instance: %w", err)
|
|
}
|
|
|
|
if applyTemplateTrigger {
|
|
err = d.DeferTemplateApply(instance.TemplateTriggerRename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if !d.IsSnapshot() {
|
|
var results []string
|
|
|
|
err := d.state.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
|
|
var err error
|
|
|
|
// Rename all the instance snapshot database entries.
|
|
results, err = tx.GetInstanceSnapshotsNames(ctx, d.project.Name, oldName)
|
|
if err != nil {
|
|
d.logger.Error("Failed to get instance snapshots", ctxMap)
|
|
|
|
return fmt.Errorf("Failed to get instance snapshots: Failed getting instance snapshot names: %w", err)
|
|
}
|
|
|
|
for _, sname := range results {
|
|
// Rename the snapshot.
|
|
oldSnapName := strings.SplitN(sname, internalInstance.SnapshotDelimiter, 2)[1]
|
|
baseSnapName := filepath.Base(sname)
|
|
|
|
err := cluster.RenameInstanceSnapshot(ctx, tx.Tx(), d.project.Name, oldName, oldSnapName, baseSnapName)
|
|
if err != nil {
|
|
d.logger.Error("Failed renaming snapshot", ctxMap)
|
|
return fmt.Errorf("Failed renaming snapshot: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Rename the instance database entry.
|
|
err = d.state.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
|
|
if d.IsSnapshot() {
|
|
oldParts := strings.SplitN(oldName, internalInstance.SnapshotDelimiter, 2)
|
|
newParts := strings.SplitN(newName, internalInstance.SnapshotDelimiter, 2)
|
|
return cluster.RenameInstanceSnapshot(ctx, tx.Tx(), d.project.Name, oldParts[0], oldParts[1], newParts[1])
|
|
}
|
|
|
|
return cluster.RenameInstance(ctx, tx.Tx(), d.project.Name, oldName, newName)
|
|
})
|
|
if err != nil {
|
|
d.logger.Error("Failed renaming instance", ctxMap)
|
|
return fmt.Errorf("Failed renaming instance: %w", err)
|
|
}
|
|
|
|
// Rename the logging path.
|
|
newFullName := project.Instance(d.Project().Name, d.Name())
|
|
_ = os.RemoveAll(internalUtil.LogPath(newFullName))
|
|
if util.PathExists(d.LogPath()) {
|
|
err := os.Rename(d.LogPath(), internalUtil.LogPath(newFullName))
|
|
if err != nil {
|
|
d.logger.Error("Failed renaming instance", ctxMap)
|
|
return fmt.Errorf("Failed renaming instance: %w", err)
|
|
}
|
|
}
|
|
|
|
// Rename the runtime path.
|
|
newFullName = project.Instance(d.Project().Name, d.Name())
|
|
_ = os.RemoveAll(internalUtil.RunPath(newFullName))
|
|
if util.PathExists(d.RunPath()) {
|
|
err := os.Rename(d.RunPath(), internalUtil.RunPath(newFullName))
|
|
if err != nil {
|
|
d.logger.Error("Failed renaming instance", ctxMap)
|
|
return fmt.Errorf("Failed renaming instance: %w", err)
|
|
}
|
|
}
|
|
|
|
reverter := revert.New()
|
|
defer reverter.Fail()
|
|
|
|
// Set the new name in the struct.
|
|
d.name = newName
|
|
reverter.Add(func() { d.name = oldName })
|
|
|
|
// Rename the backups.
|
|
backups, err := d.Backups()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, backup := range backups {
|
|
b := backup
|
|
oldName := b.Name()
|
|
backupName := strings.Split(oldName, "/")[1]
|
|
newName := fmt.Sprintf("%s/%s", newName, backupName)
|
|
|
|
err = b.Rename(newName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
reverter.Add(func() { _ = b.Rename(oldName) })
|
|
}
|
|
|
|
// Invalidate the go-lxc cache.
|
|
d.release()
|
|
|
|
d.cConfig = false
|
|
|
|
// Update lease files.
|
|
err = network.UpdateDNSMasqStatic(d.state, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Reset cloud-init instance-id (causes a re-run on name changes).
|
|
if !d.IsSnapshot() {
|
|
err = d.resetInstanceID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Update the backup file.
|
|
err = d.UpdateBackupFile()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
d.logger.Info("Renamed instance", ctxMap)
|
|
if d.isSnapshot {
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceSnapshotRenamed.Event(d, map[string]any{"old_name": oldName}))
|
|
} else {
|
|
err = d.state.Authorizer.RenameInstance(d.state.ShutdownCtx, d.project.Name, oldName, newName)
|
|
if err != nil {
|
|
logger.Error("Failed to rename instance in authorizer", logger.Ctx{"old_name": oldName, "new_name": newName, "project": d.project.Name, "error": err})
|
|
}
|
|
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceRenamed.Event(d, map[string]any{"old_name": oldName}))
|
|
}
|
|
|
|
reverter.Success()
|
|
|
|
return nil
|
|
}
|
|
|
|
// CGroupSet sets a cgroup value for the instance.
|
|
func (d *lxc) CGroupSet(key string, value string) error {
|
|
// Load the go-lxc struct
|
|
cc, err := d.initLXC(false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Make sure the container is running.
|
|
// We use InitPID here rather than IsRunning because this task can be triggered during the container's
|
|
// startup process, which is during the time that the start lock is held, which causes IsRunning to
|
|
// return false (because the container hasn't fully started yet) but it is sufficiently started to
|
|
// have its cgroup disk limits set.
|
|
if d.InitPID() <= 0 {
|
|
return errors.New("Can't set cgroups on a stopped container")
|
|
}
|
|
|
|
err = cc.SetCgroupItem(key, value)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to set cgroup %s=\"%s\": %w", key, value, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Update applies updated config.
|
|
func (d *lxc) Update(args db.InstanceArgs, userRequested bool) error {
|
|
// Setup a new operation
|
|
op, err := operationlock.CreateWaitGet(d.Project().Name, d.Name(), d.op, operationlock.ActionUpdate, []operationlock.Action{operationlock.ActionCreate, operationlock.ActionRestart, operationlock.ActionRestore}, false, false)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to create instance update operation: %w", err)
|
|
}
|
|
|
|
defer op.Done(nil)
|
|
|
|
// Set sane defaults for unset keys
|
|
if args.Project == "" {
|
|
args.Project = api.ProjectDefaultName
|
|
}
|
|
|
|
if args.Architecture == 0 {
|
|
args.Architecture = d.architecture
|
|
}
|
|
|
|
if args.Config == nil {
|
|
args.Config = map[string]string{}
|
|
}
|
|
|
|
if args.Devices == nil {
|
|
args.Devices = deviceConfig.Devices{}
|
|
}
|
|
|
|
if args.Profiles == nil {
|
|
args.Profiles = []api.Profile{}
|
|
}
|
|
|
|
if userRequested {
|
|
// Validate the new config
|
|
err := instance.ValidConfig(d.state.OS, args.Config, false, d.dbType)
|
|
if err != nil {
|
|
return fmt.Errorf("Invalid config: %w", err)
|
|
}
|
|
|
|
// Validate the new devices without using expanded devices validation (expensive checks disabled).
|
|
err = instance.ValidDevices(d.state, d.project, d.Type(), args.Devices, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("Invalid devices: %w", err)
|
|
}
|
|
}
|
|
|
|
var profiles []string
|
|
|
|
err = d.state.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
|
|
// Validate the new profiles
|
|
profiles, err = tx.GetProfileNames(ctx, args.Project)
|
|
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to get profiles: %w", err)
|
|
}
|
|
|
|
checkedProfiles := []string{}
|
|
for _, profile := range args.Profiles {
|
|
if !slices.Contains(profiles, profile.Name) {
|
|
return fmt.Errorf("Requested profile '%s' doesn't exist", profile.Name)
|
|
}
|
|
|
|
if slices.Contains(checkedProfiles, profile.Name) {
|
|
return errors.New("Duplicate profile found in request")
|
|
}
|
|
|
|
checkedProfiles = append(checkedProfiles, profile.Name)
|
|
}
|
|
|
|
// Validate the new architecture
|
|
if args.Architecture != 0 {
|
|
_, err = osarch.ArchitectureName(args.Architecture)
|
|
if err != nil {
|
|
return fmt.Errorf("Invalid architecture id: %s", err)
|
|
}
|
|
}
|
|
|
|
// Get a copy of the old configuration
|
|
oldDescription := d.Description()
|
|
oldArchitecture := 0
|
|
err = util.DeepCopy(&d.architecture, &oldArchitecture)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
oldEphemeral := false
|
|
err = util.DeepCopy(&d.ephemeral, &oldEphemeral)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
oldExpandedDevices := deviceConfig.Devices{}
|
|
err = util.DeepCopy(&d.expandedDevices, &oldExpandedDevices)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
oldExpandedConfig := map[string]string{}
|
|
err = util.DeepCopy(&d.expandedConfig, &oldExpandedConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
oldLocalDevices := deviceConfig.Devices{}
|
|
err = util.DeepCopy(&d.localDevices, &oldLocalDevices)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
oldLocalConfig := map[string]string{}
|
|
err = util.DeepCopy(&d.localConfig, &oldLocalConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
oldProfiles := []api.Profile{}
|
|
err = util.DeepCopy(&d.profiles, &oldProfiles)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
oldExpiryDate := d.expiryDate
|
|
|
|
// Define a function which reverts everything. Defer this function
|
|
// so that it doesn't need to be explicitly called in every failing
|
|
// return path. Track whether or not we want to undo the changes
|
|
// using a closure.
|
|
undoChanges := true
|
|
defer func() {
|
|
if undoChanges {
|
|
d.description = oldDescription
|
|
d.architecture = oldArchitecture
|
|
d.ephemeral = oldEphemeral
|
|
d.expandedConfig = oldExpandedConfig
|
|
d.expandedDevices = oldExpandedDevices
|
|
d.localConfig = oldLocalConfig
|
|
d.localDevices = oldLocalDevices
|
|
d.profiles = oldProfiles
|
|
d.expiryDate = oldExpiryDate
|
|
d.release()
|
|
d.cConfig = false
|
|
_, _ = d.initLXC(true)
|
|
_ = d.setupCredentials(true)
|
|
cgroup.TaskSchedulerTrigger("container", d.name, "changed")
|
|
}
|
|
}()
|
|
|
|
// Apply the various changes
|
|
d.description = args.Description
|
|
d.architecture = args.Architecture
|
|
d.ephemeral = args.Ephemeral
|
|
d.localConfig = args.Config
|
|
d.localDevices = args.Devices
|
|
d.profiles = args.Profiles
|
|
d.expiryDate = args.ExpiryDate
|
|
|
|
// Expand the config and refresh the LXC config
|
|
err = d.expandConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Diff the configurations
|
|
changedConfig := []string{}
|
|
for key := range oldExpandedConfig {
|
|
if oldExpandedConfig[key] != d.expandedConfig[key] {
|
|
if !slices.Contains(changedConfig, key) {
|
|
changedConfig = append(changedConfig, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
for key := range d.expandedConfig {
|
|
if oldExpandedConfig[key] != d.expandedConfig[key] {
|
|
if !slices.Contains(changedConfig, key) {
|
|
changedConfig = append(changedConfig, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Diff the devices
|
|
removeDevices, addDevices, updateDevices, allUpdatedKeys := oldExpandedDevices.Update(d.expandedDevices, func(oldDevice deviceConfig.Device, newDevice deviceConfig.Device) []string {
|
|
// This function needs to return a list of fields that are excluded from differences
|
|
// between oldDevice and newDevice. The result of this is that as long as the
|
|
// devices are otherwise identical except for the fields returned here, then the
|
|
// device is considered to be being "updated" rather than "added & removed".
|
|
oldDevType, err := device.LoadByType(d.state, d.Project().Name, oldDevice)
|
|
if err != nil {
|
|
return []string{} // Couldn't create Device, so this cannot be an update.
|
|
}
|
|
|
|
newDevType, err := device.LoadByType(d.state, d.Project().Name, newDevice)
|
|
if err != nil {
|
|
return []string{} // Couldn't create Device, so this cannot be an update.
|
|
}
|
|
|
|
// Detached devices need to be fully recreated on update so that the update logic doesn't
|
|
// try to access non-existing LXC devices.
|
|
if !util.IsTrueOrEmpty(oldDevice["attached"]) {
|
|
return []string{}
|
|
}
|
|
|
|
return newDevType.UpdatableFields(oldDevType)
|
|
})
|
|
|
|
// Prevent adding or updating device initial configuration.
|
|
if util.StringPrefixInSlice("initial.", allUpdatedKeys) {
|
|
for devName, newDev := range addDevices {
|
|
for k, newVal := range newDev {
|
|
if !strings.HasPrefix(k, "initial.") {
|
|
continue
|
|
}
|
|
|
|
oldDev, ok := removeDevices[devName]
|
|
if !ok {
|
|
return errors.New("New device with initial configuration cannot be added once the instance is created")
|
|
}
|
|
|
|
oldVal, ok := oldDev[k]
|
|
if !ok {
|
|
return errors.New("Device initial configuration cannot be added once the instance is created")
|
|
}
|
|
|
|
// If newVal is an empty string it means the initial configuration
|
|
// has been removed.
|
|
if newVal != "" && newVal != oldVal {
|
|
return errors.New("Device initial configuration cannot be modified once the instance is created")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if userRequested {
|
|
// Look for deleted idmap keys.
|
|
protectedKeys := []string{
|
|
"volatile.idmap.base",
|
|
"volatile.idmap.current",
|
|
"volatile.idmap.next",
|
|
"volatile.last_state.idmap",
|
|
}
|
|
|
|
for _, k := range changedConfig {
|
|
if !slices.Contains(protectedKeys, k) {
|
|
continue
|
|
}
|
|
|
|
_, ok := d.expandedConfig[k]
|
|
if !ok {
|
|
return errors.New("Volatile idmap keys can't be deleted by the user")
|
|
}
|
|
}
|
|
|
|
// Do some validation of the config diff (allows mixed instance types for profiles).
|
|
err = instance.ValidConfig(d.state.OS, d.expandedConfig, true, instancetype.Any)
|
|
if err != nil {
|
|
return fmt.Errorf("Invalid expanded config: %w", err)
|
|
}
|
|
|
|
// Do full expanded validation of the devices diff.
|
|
err = instance.ValidDevices(d.state, d.project, d.Type(), d.localDevices, d.expandedDevices)
|
|
if err != nil {
|
|
return fmt.Errorf("Invalid expanded devices: %w", err)
|
|
}
|
|
|
|
// Validate root device
|
|
_, oldRootDev, oldErr := internalInstance.GetRootDiskDevice(oldExpandedDevices.CloneNative())
|
|
_, newRootDev, newErr := internalInstance.GetRootDiskDevice(d.expandedDevices.CloneNative())
|
|
if oldErr == nil && newErr == nil && oldRootDev["pool"] != newRootDev["pool"] {
|
|
return fmt.Errorf("Cannot update root disk device pool name to %q", newRootDev["pool"])
|
|
}
|
|
|
|
// Ensure the instance has a root disk.
|
|
if newErr != nil {
|
|
return fmt.Errorf("Invalid root disk device: %w", newErr)
|
|
}
|
|
}
|
|
|
|
// Run through initLXC to catch anything we missed
|
|
if userRequested {
|
|
d.release()
|
|
d.cConfig = false
|
|
_, err = d.initLXC(true)
|
|
if err != nil {
|
|
return fmt.Errorf("Initialize LXC: %w", err)
|
|
}
|
|
}
|
|
|
|
// If raw.lxc changed, re-validate the config.
|
|
if slices.Contains(changedConfig, "raw.lxc") && d.expandedConfig["raw.lxc"] != "" {
|
|
// Get a new liblxc instance.
|
|
cc, err := liblxc.NewContainer(d.name, d.state.OS.LxcPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = d.loadRawLXCConfig(cc)
|
|
if err != nil {
|
|
// Release the liblxc instance.
|
|
_ = cc.Release()
|
|
return err
|
|
}
|
|
|
|
// Release the liblxc instance.
|
|
_ = cc.Release()
|
|
}
|
|
|
|
// If apparmor changed, re-validate the apparmor profile (even if not running).
|
|
if slices.Contains(changedConfig, "raw.apparmor") || slices.Contains(changedConfig, "security.nesting") {
|
|
err = apparmor.InstanceValidate(d.state.OS, d, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("Parse AppArmor profile: %w", err)
|
|
}
|
|
}
|
|
|
|
if slices.Contains(changedConfig, "security.idmap.isolated") || slices.Contains(changedConfig, "security.idmap.base") || slices.Contains(changedConfig, "security.idmap.size") || slices.Contains(changedConfig, "raw.idmap") || slices.Contains(changedConfig, "security.privileged") {
|
|
var idmapSet *idmap.Set
|
|
base := int64(0)
|
|
if !d.IsPrivileged() {
|
|
// Update the idmap.
|
|
idmapSet, base, err = d.findIdmap()
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to get ID map: %w", err)
|
|
}
|
|
}
|
|
|
|
jsonIdmap, err := idmapSet.ToJSON()
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to encode ID map: %w", err)
|
|
}
|
|
|
|
d.localConfig["volatile.idmap.next"] = jsonIdmap
|
|
d.localConfig["volatile.idmap.base"] = fmt.Sprintf("%v", base)
|
|
|
|
// Invalidate the idmap cache.
|
|
d.idmapset = nil
|
|
}
|
|
|
|
isRunning := d.IsRunning()
|
|
|
|
// Use the device interface to apply update changes.
|
|
err = d.devicesUpdate(d, removeDevices, addDevices, updateDevices, oldExpandedDevices, isRunning, userRequested)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Apply the live changes
|
|
if isRunning {
|
|
cc, err := d.initLXC(false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cg, err := d.cgroup(cc, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Live update the container config
|
|
for _, key := range changedConfig {
|
|
value := d.expandedConfig[key]
|
|
|
|
if key == "raw.apparmor" || key == "security.nesting" {
|
|
// Update the AppArmor profile
|
|
err = apparmor.InstanceLoad(d.state.OS, d, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else if key == "security.guestapi" {
|
|
if util.IsTrueOrEmpty(value) {
|
|
err = d.insertMount(internalUtil.VarPath("guestapi"), "/dev/incus", "none", unix.MS_BIND, idmap.StorageTypeNone)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// Connect to files API.
|
|
files, err := d.FileSFTP()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() { _ = files.Close() }()
|
|
|
|
_, err = files.Lstat("/dev/incus")
|
|
if err == nil {
|
|
err = d.removeMount("/dev/incus")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = files.Remove("/dev/incus")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
} else if key == "linux.kernel_modules" && value != "" {
|
|
for _, module := range strings.Split(value, ",") {
|
|
module = strings.TrimPrefix(module, " ")
|
|
err := linux.LoadModule(module)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to load kernel module '%s': %w", module, err)
|
|
}
|
|
}
|
|
} else if key == "limits.disk.priority" {
|
|
if !d.state.OS.CGInfo.Supports(cgroup.Blkio, cg) {
|
|
continue
|
|
}
|
|
|
|
priorityInt := 5
|
|
diskPriority := d.expandedConfig["limits.disk.priority"]
|
|
if diskPriority != "" {
|
|
priorityInt, err = strconv.Atoi(diskPriority)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Minimum valid value is 10
|
|
priority := int64(priorityInt * 100)
|
|
if priority == 0 {
|
|
priority = 10
|
|
}
|
|
|
|
err = cg.SetBlkioWeight(priority)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else if key == "limits.memory.oom_priority" {
|
|
// Configure the OOM priority.
|
|
err = d.setOOMPriority(cc.InitPid())
|
|
if err != nil {
|
|
d.logger.Warn("Failed to set OOM priority", logger.Ctx{
|
|
"err": err,
|
|
"instance": d.Name(),
|
|
"project": d.Project().Name,
|
|
})
|
|
}
|
|
} else if key == "limits.memory" || strings.HasPrefix(key, "limits.memory.") {
|
|
// Skip if no memory CGroup
|
|
if !d.state.OS.CGInfo.Supports(cgroup.Memory, cg) {
|
|
continue
|
|
}
|
|
|
|
// Set the new memory limit
|
|
memory := d.expandedConfig["limits.memory"]
|
|
memoryEnforce := d.expandedConfig["limits.memory.enforce"]
|
|
memorySwap := d.expandedConfig["limits.memory.swap"]
|
|
var memoryInt int64
|
|
|
|
// Parse memory
|
|
if memory == "" {
|
|
memoryInt = -1
|
|
} else {
|
|
memoryInt, err = ParseMemoryStr(memory)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Store the old values for revert
|
|
oldMemswLimit := int64(-1)
|
|
if d.state.OS.CGInfo.Supports(cgroup.MemorySwap, cg) {
|
|
oldMemswLimit, err = cg.GetMemorySwapLimit()
|
|
if err != nil {
|
|
oldMemswLimit = -1
|
|
}
|
|
}
|
|
oldLimit, err := cg.GetMemoryLimit()
|
|
if err != nil {
|
|
oldLimit = -1
|
|
}
|
|
|
|
oldSoftLimit, err := cg.GetMemorySoftLimit()
|
|
if err != nil {
|
|
oldSoftLimit = -1
|
|
}
|
|
|
|
revertMemory := func() {
|
|
if oldSoftLimit != -1 {
|
|
_ = cg.SetMemorySoftLimit(oldSoftLimit)
|
|
}
|
|
|
|
if oldLimit != -1 {
|
|
_ = cg.SetMemoryLimit(oldLimit)
|
|
}
|
|
|
|
if oldMemswLimit != -1 {
|
|
_ = cg.SetMemorySwapLimit(oldMemswLimit)
|
|
}
|
|
}
|
|
|
|
// Reset everything
|
|
if d.state.OS.CGInfo.Supports(cgroup.MemorySwap, cg) {
|
|
err = cg.SetMemorySwapLimit(-1)
|
|
if err != nil {
|
|
revertMemory()
|
|
return err
|
|
}
|
|
}
|
|
|
|
err = cg.SetMemoryLimit(-1)
|
|
if err != nil {
|
|
revertMemory()
|
|
return err
|
|
}
|
|
|
|
err = cg.SetMemorySoftLimit(-1)
|
|
if err != nil {
|
|
revertMemory()
|
|
return err
|
|
}
|
|
|
|
// Set the new values
|
|
if memoryEnforce == "soft" {
|
|
// Set new limit.
|
|
err = cg.SetMemorySoftLimit(memoryInt)
|
|
if err != nil {
|
|
revertMemory()
|
|
return err
|
|
}
|
|
} else {
|
|
err = cg.SetMemoryLimit(memoryInt)
|
|
if err != nil {
|
|
revertMemory()
|
|
return err
|
|
}
|
|
|
|
if d.state.OS.CGInfo.Supports(cgroup.MemorySwap, cg) {
|
|
if util.IsTrueOrEmpty(memorySwap) || util.IsFalse(memorySwap) {
|
|
err = cg.SetMemorySwapLimit(0)
|
|
if err != nil {
|
|
revertMemory()
|
|
return err
|
|
}
|
|
} else {
|
|
// Additional memory as swap.
|
|
swapInt, err := units.ParseByteSizeString(memorySwap)
|
|
if err != nil {
|
|
revertMemory()
|
|
return err
|
|
}
|
|
|
|
err = cg.SetMemorySwapLimit(swapInt)
|
|
if err != nil {
|
|
revertMemory()
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// If on Cgroup1, set soft limit to value 10% less than hard limit.
|
|
if memoryInt > 0 && slices.Contains([]cgroup.Layout{cgroup.CgroupsLegacy, cgroup.CgroupsHybrid}, d.state.OS.CGInfo.Layout) {
|
|
err = cg.SetMemorySoftLimit(int64(float64(memoryInt) * 0.9))
|
|
if err != nil {
|
|
revertMemory()
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if !d.state.OS.CGInfo.Supports(cgroup.MemorySwappiness, cg) {
|
|
continue
|
|
}
|
|
|
|
// Configure the swappiness
|
|
if key == "limits.memory.swap" || key == "limits.memory.swap.priority" {
|
|
memorySwapPriority := d.expandedConfig["limits.memory.swap.priority"]
|
|
if util.IsFalse(memorySwap) {
|
|
err = cg.SetMemorySwappiness(0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
priority := 10
|
|
if memorySwapPriority != "" {
|
|
priority, err = strconv.Atoi(memorySwapPriority)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Maximum priority (10) should be default swappiness (60).
|
|
err = cg.SetMemorySwappiness(int64(70 - priority))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
} else if key == "limits.cpu" || key == "limits.cpu.nodes" {
|
|
// Clear the "volatile.cpu.nodes" if needed.
|
|
d.ClearLimitsCPUNodes(changedConfig)
|
|
|
|
// Trigger a scheduler re-run
|
|
defer cgroup.TaskSchedulerTrigger("container", d.name, "changed") //nolint:revive
|
|
} else if key == "limits.cpu.priority" || key == "limits.cpu.allowance" {
|
|
// Skip if no cpu CGroup
|
|
if !d.state.OS.CGInfo.Supports(cgroup.CPU, cg) {
|
|
continue
|
|
}
|
|
|
|
// Apply new CPU limits
|
|
cpuShares, cpuCfsQuota, cpuCfsPeriod, err := cgroup.ParseCPU(d.expandedConfig["limits.cpu.allowance"], d.expandedConfig["limits.cpu.priority"])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = cg.SetCPUShare(cpuShares)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = cg.SetCPUCfsLimit(cpuCfsPeriod, cpuCfsQuota)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else if key == "limits.processes" {
|
|
if !d.state.OS.CGInfo.Supports(cgroup.Pids, cg) {
|
|
continue
|
|
}
|
|
|
|
if value == "" {
|
|
err = cg.SetMaxProcesses(-1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
valueInt, err := strconv.ParseInt(value, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = cg.SetMaxProcesses(valueInt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else if strings.HasPrefix(key, "limits.hugepages.") {
|
|
if !d.state.OS.CGInfo.Supports(cgroup.Hugetlb, cg) {
|
|
continue
|
|
}
|
|
|
|
pageType := ""
|
|
|
|
switch key {
|
|
case "limits.hugepages.64KB":
|
|
pageType = "64KB"
|
|
case "limits.hugepages.1MB":
|
|
pageType = "1MB"
|
|
case "limits.hugepages.2MB":
|
|
pageType = "2MB"
|
|
case "limits.hugepages.1GB":
|
|
pageType = "1GB"
|
|
}
|
|
|
|
valueInt := int64(-1)
|
|
if value != "" {
|
|
valueInt, err = units.ParseByteSizeString(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
err = cg.SetHugepagesLimit(pageType, valueInt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update the credentials directory.
|
|
err = d.setupCredentials(true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Re-generate the instance-id if needed.
|
|
if !d.IsSnapshot() && d.needsNewInstanceID(changedConfig, oldExpandedDevices) {
|
|
err = d.resetInstanceID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Finally, apply the changes to the database
|
|
err = d.state.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
|
|
// Snapshots should update only their descriptions and expiry date.
|
|
if d.IsSnapshot() {
|
|
return tx.UpdateInstanceSnapshot(d.id, d.description, d.expiryDate)
|
|
}
|
|
|
|
object, err := cluster.GetInstance(ctx, tx.Tx(), d.project.Name, d.name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
object.Description = d.description
|
|
object.Architecture = d.architecture
|
|
object.Ephemeral = d.ephemeral
|
|
object.ExpiryDate = sql.NullTime{Time: d.expiryDate, Valid: true}
|
|
|
|
err = cluster.UpdateInstance(ctx, tx.Tx(), d.project.Name, d.name, *object)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = cluster.UpdateInstanceConfig(ctx, tx.Tx(), int64(object.ID), d.localConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
devices, err := cluster.APIToDevices(d.localDevices.CloneNative())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = cluster.UpdateInstanceDevices(ctx, tx.Tx(), int64(object.ID), devices)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
profileNames := make([]string, 0, len(d.profiles))
|
|
for _, profile := range d.profiles {
|
|
profileNames = append(profileNames, profile.Name)
|
|
}
|
|
|
|
return cluster.UpdateInstanceProfiles(ctx, tx.Tx(), object.ID, object.Project, profileNames)
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to update database: %w", err)
|
|
}
|
|
|
|
err = d.UpdateBackupFile()
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return fmt.Errorf("Failed to write backup file: %w", err)
|
|
}
|
|
|
|
// Send devIncus notifications
|
|
if isRunning {
|
|
// Config changes (only for user.* keys
|
|
for _, key := range changedConfig {
|
|
if !strings.HasPrefix(key, "user.") {
|
|
continue
|
|
}
|
|
|
|
msg := map[string]any{
|
|
"key": key,
|
|
"old_value": oldExpandedConfig[key],
|
|
"value": d.expandedConfig[key],
|
|
}
|
|
|
|
err = d.devIncusEventSend("config", msg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Device changes
|
|
for k, m := range removeDevices {
|
|
msg := map[string]any{
|
|
"action": "removed",
|
|
"name": k,
|
|
"config": m,
|
|
}
|
|
|
|
err = d.devIncusEventSend("device", msg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for k, m := range updateDevices {
|
|
msg := map[string]any{
|
|
"action": "updated",
|
|
"name": k,
|
|
"config": m,
|
|
}
|
|
|
|
err = d.devIncusEventSend("device", msg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for k, m := range addDevices {
|
|
msg := map[string]any{
|
|
"action": "added",
|
|
"name": k,
|
|
"config": m,
|
|
}
|
|
|
|
err = d.devIncusEventSend("device", msg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Success, update the closure to mark that the changes should be kept.
|
|
undoChanges = false
|
|
|
|
if userRequested {
|
|
if d.isSnapshot {
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceSnapshotUpdated.Event(d, nil))
|
|
} else {
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceUpdated.Event(d, nil))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Export backs up the instance.
|
|
func (d *lxc) Export(metaWriter io.Writer, rootfsWriter io.Writer, properties map[string]string, expiration time.Time, tracker *ioprogress.ProgressTracker) (*api.ImageMetadata, error) {
|
|
ctxMap := logger.Ctx{
|
|
"created": d.creationDate,
|
|
"ephemeral": d.ephemeral,
|
|
"used": d.lastUsedDate,
|
|
}
|
|
|
|
if d.IsRunning() {
|
|
return nil, errors.New("Cannot export a running instance as an image")
|
|
}
|
|
|
|
d.logger.Info("Exporting instance", ctxMap)
|
|
|
|
// Start the storage.
|
|
_, err := d.mount()
|
|
if err != nil {
|
|
d.logger.Error("Failed exporting instance", ctxMap)
|
|
return nil, err
|
|
}
|
|
|
|
defer func() { _ = d.unmount() }()
|
|
|
|
// Get IDMap to unshift container as the tarball is created.
|
|
idmap, err := d.DiskIdmap()
|
|
if err != nil {
|
|
d.logger.Error("Failed exporting instance", ctxMap)
|
|
return nil, err
|
|
}
|
|
|
|
// Create the tarball.
|
|
metaTarWriter := instancewriter.NewInstanceTarWriter(metaWriter, idmap)
|
|
|
|
var rootfsTarWriter *instancewriter.InstanceTarWriter
|
|
if rootfsWriter != nil {
|
|
rootfsTarWriter = instancewriter.NewInstanceTarWriter(rootfsWriter, idmap)
|
|
}
|
|
|
|
// Keep track of the first path we saw for each path with nlink>1.
|
|
cDir := d.Path()
|
|
|
|
// Path inside the tar image is the pathname starting after cDir.
|
|
// For the rootfs tarball in a split image, the path inside is the pathname starting after rootfs/
|
|
metaOffset := len(cDir) + 1
|
|
rootfsOffset := len(d.RootfsPath())
|
|
|
|
writeToMetaTar := func(fPath string, fi os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = metaTarWriter.WriteFile(fPath[metaOffset:], fPath, fi, false)
|
|
if err != nil {
|
|
d.logger.Debug("Error tarring up", logger.Ctx{"path": fPath, "err": err})
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var writeToRootfsTar func(string, os.FileInfo, error) error
|
|
if rootfsWriter != nil {
|
|
writeToRootfsTar = func(path string, fi os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = rootfsTarWriter.WriteFile(path[rootfsOffset:], path, fi, false)
|
|
if err != nil {
|
|
d.logger.Debug("Error tarring up", logger.Ctx{"path": path, "err": err})
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Get the instance's architecture.
|
|
var arch string
|
|
if d.IsSnapshot() {
|
|
parentName, _, _ := api.GetParentAndSnapshotName(d.name)
|
|
parent, err := instance.LoadByProjectAndName(d.state, d.project.Name, parentName)
|
|
if err != nil {
|
|
_ = metaTarWriter.Close()
|
|
if rootfsTarWriter != nil {
|
|
_ = rootfsTarWriter.Close()
|
|
}
|
|
|
|
d.logger.Error("Failed exporting instance", ctxMap)
|
|
return nil, err
|
|
}
|
|
|
|
arch, _ = osarch.ArchitectureName(parent.Architecture())
|
|
} else {
|
|
arch, _ = osarch.ArchitectureName(d.architecture)
|
|
}
|
|
|
|
if arch == "" {
|
|
arch, err = osarch.ArchitectureName(d.state.OS.Architectures[0])
|
|
if err != nil {
|
|
d.logger.Error("Failed exporting instance", ctxMap)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Generate metadata.yaml.
|
|
meta := api.ImageMetadata{}
|
|
fnam := filepath.Join(cDir, "metadata.yaml")
|
|
|
|
if util.PathExists(fnam) {
|
|
// Parse the metadata.
|
|
content, err := os.ReadFile(fnam)
|
|
if err != nil {
|
|
_ = metaTarWriter.Close()
|
|
if rootfsTarWriter != nil {
|
|
_ = rootfsTarWriter.Close()
|
|
}
|
|
|
|
d.logger.Error("Failed exporting instance", ctxMap)
|
|
return nil, err
|
|
}
|
|
|
|
err = yaml.Unmarshal(content, &meta)
|
|
if err != nil {
|
|
_ = metaTarWriter.Close()
|
|
if rootfsTarWriter != nil {
|
|
_ = rootfsTarWriter.Close()
|
|
}
|
|
|
|
d.logger.Error("Failed exporting instance", ctxMap)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Fill in the metadata.
|
|
meta.Architecture = arch
|
|
meta.CreationDate = time.Now().UTC().Unix()
|
|
|
|
if meta.Properties == nil {
|
|
meta.Properties = map[string]string{}
|
|
}
|
|
|
|
maps.Copy(meta.Properties, properties)
|
|
|
|
if !expiration.IsZero() {
|
|
meta.ExpiryDate = expiration.UTC().Unix()
|
|
}
|
|
|
|
// Write the new metadata.yaml.
|
|
tempDir, err := os.MkdirTemp("", "incus_metadata_")
|
|
if err != nil {
|
|
_ = metaTarWriter.Close()
|
|
if rootfsTarWriter != nil {
|
|
_ = rootfsTarWriter.Close()
|
|
}
|
|
|
|
d.logger.Error("Failed exporting instance", ctxMap)
|
|
return nil, err
|
|
}
|
|
|
|
defer func() { _ = os.RemoveAll(tempDir) }()
|
|
|
|
data, err := yaml.Marshal(&meta)
|
|
if err != nil {
|
|
_ = metaTarWriter.Close()
|
|
if rootfsTarWriter != nil {
|
|
_ = rootfsTarWriter.Close()
|
|
}
|
|
|
|
d.logger.Error("Failed exporting instance", ctxMap)
|
|
return nil, err
|
|
}
|
|
|
|
fnam = filepath.Join(tempDir, "metadata.yaml")
|
|
err = os.WriteFile(fnam, data, 0o644)
|
|
if err != nil {
|
|
_ = metaTarWriter.Close()
|
|
if rootfsTarWriter != nil {
|
|
_ = rootfsTarWriter.Close()
|
|
}
|
|
|
|
d.logger.Error("Failed exporting instance", ctxMap)
|
|
return nil, err
|
|
}
|
|
|
|
// Add metadata.yaml to the tarball.
|
|
fi, err := os.Lstat(fnam)
|
|
if err != nil {
|
|
_ = metaTarWriter.Close()
|
|
if rootfsTarWriter != nil {
|
|
_ = rootfsTarWriter.Close()
|
|
}
|
|
|
|
d.logger.Error("Failed exporting instance", ctxMap)
|
|
return nil, err
|
|
}
|
|
|
|
tmpOffset := len(filepath.Dir(fnam)) + 1
|
|
err = metaTarWriter.WriteFile(fnam[tmpOffset:], fnam, fi, false)
|
|
if err != nil {
|
|
_ = metaTarWriter.Close()
|
|
if rootfsTarWriter != nil {
|
|
_ = rootfsTarWriter.Close()
|
|
}
|
|
|
|
d.logger.Debug("Error writing to tarfile", logger.Ctx{"err": err})
|
|
d.logger.Error("Failed exporting instance", ctxMap)
|
|
return nil, err
|
|
}
|
|
|
|
// Include all the rootfs files.
|
|
fnam = d.RootfsPath()
|
|
if rootfsWriter == nil {
|
|
err = filepath.Walk(fnam, writeToMetaTar)
|
|
if err != nil {
|
|
d.logger.Error("Failed exporting instance", ctxMap)
|
|
return nil, err
|
|
}
|
|
} else {
|
|
err = filepath.Walk(fnam, writeToRootfsTar)
|
|
if err != nil {
|
|
d.logger.Error("Failed exporting instance", ctxMap)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Include all the templates.
|
|
fnam = d.TemplatesPath()
|
|
if util.PathExists(fnam) {
|
|
err = filepath.Walk(fnam, writeToMetaTar)
|
|
if err != nil {
|
|
d.logger.Error("Failed exporting instance", ctxMap)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
err = metaTarWriter.Close()
|
|
if err != nil {
|
|
d.logger.Error("Failed exporting instance", ctxMap)
|
|
return nil, err
|
|
}
|
|
|
|
if rootfsTarWriter != nil {
|
|
err = rootfsTarWriter.Close()
|
|
if err != nil {
|
|
d.logger.Error("Failed exporting instance", ctxMap)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
d.logger.Info("Exported instance", ctxMap)
|
|
return &meta, nil
|
|
}
|
|
|
|
func collectCRIULogFile(d instance.Instance, imagesDir string, function string, method string) error {
|
|
t := time.Now().Format(time.RFC3339)
|
|
newPath := filepath.Join(d.LogPath(), fmt.Sprintf("%s_%s_%s.log", function, method, t))
|
|
return internalUtil.FileCopy(filepath.Join(imagesDir, fmt.Sprintf("%s.log", method)), newPath)
|
|
}
|
|
|
|
func getCRIULogErrors(imagesDir string, method string) (string, error) {
|
|
f, err := os.Open(path.Join(imagesDir, fmt.Sprintf("%s.log", method)))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
defer func() { _ = f.Close() }()
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
ret := []string{}
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if strings.Contains(line, "Error") || strings.Contains(line, "Warn") {
|
|
ret = append(ret, scanner.Text())
|
|
}
|
|
}
|
|
|
|
return strings.Join(ret, "\n"), nil
|
|
}
|
|
|
|
// Check if CRIU supports pre-dumping and number of pre-dump iterations.
|
|
func (d *lxc) migrationSendCheckForPreDumpSupport() (bool, int) {
|
|
// Check if this architecture/kernel/criu combination supports pre-copy dirty memory tracking feature.
|
|
_, err := subprocess.RunCommand("criu", "check", "--feature", "mem_dirty_track")
|
|
if err != nil {
|
|
// CRIU says it does not know about dirty memory tracking.
|
|
// This means the rest of this function is irrelevant.
|
|
return false, 0
|
|
}
|
|
|
|
// CRIU says it can actually do pre-dump. Let's set it to true
|
|
// unless the user wants something else.
|
|
usePreDumps := true
|
|
|
|
// What does the configuration say about pre-copy
|
|
tmp := d.ExpandedConfig()["migration.incremental.memory"]
|
|
|
|
if tmp != "" {
|
|
usePreDumps = util.IsTrue(tmp)
|
|
}
|
|
|
|
var maxIterations int
|
|
|
|
// migration.incremental.memory.iterations is the value after which the
|
|
// container will be definitely migrated, even if the remaining number
|
|
// of memory pages is below the defined threshold.
|
|
tmp = d.ExpandedConfig()["migration.incremental.memory.iterations"]
|
|
if tmp != "" {
|
|
maxIterations, _ = strconv.Atoi(tmp)
|
|
} else {
|
|
// default to 10
|
|
maxIterations = 10
|
|
}
|
|
|
|
if maxIterations > 999 {
|
|
// the pre-dump directory is hardcoded to a string
|
|
// with maximal 3 digits. 999 pre-dumps makes no
|
|
// sense at all, but let's make sure the number
|
|
// is not higher than this.
|
|
maxIterations = 999
|
|
}
|
|
|
|
logger.Debugf("Using maximal %d iterations for pre-dumping", maxIterations)
|
|
|
|
return usePreDumps, maxIterations
|
|
}
|
|
|
|
func (d *lxc) migrationSendWriteActionScript(directory string, operation string, secret string, execPath string) error {
|
|
script := fmt.Sprintf(`#!/bin/sh -e
|
|
if [ "$CRTOOLS_SCRIPT_ACTION" = "post-dump" ]; then
|
|
%s migratedumpsuccess %s %s
|
|
fi
|
|
`, execPath, operation, secret)
|
|
|
|
f, err := os.Create(filepath.Join(directory, "action.sh"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = f.Chmod(0o500)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = f.WriteString(script)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return f.Close()
|
|
}
|
|
|
|
func (d *lxc) MigrateSend(args instance.MigrateSendArgs) error {
|
|
d.logger.Debug("Migration send starting")
|
|
defer d.logger.Debug("Migration send stopped")
|
|
|
|
// Check for an existing operation.
|
|
|
|
// Setup a new operation.
|
|
op := operationlock.Get(d.Project().Name, d.Name())
|
|
if op != nil && op.ActionMatch(operationlock.ActionMigrate) {
|
|
return errors.New("The instance is already being migrated")
|
|
}
|
|
|
|
op, err := operationlock.CreateWaitGet(d.Project().Name, d.Name(), d.op, operationlock.ActionMigrate, nil, false, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If not running, stop any forkfile instance.
|
|
if !d.IsRunning() {
|
|
d.stopForkfile(false)
|
|
}
|
|
|
|
// Wait for essential migration connections before negotiation.
|
|
connectionsCtx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
|
defer cancel()
|
|
|
|
filesystemConn, err := args.FilesystemConn(connectionsCtx)
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
var stateConn io.ReadWriteCloser
|
|
if args.Live {
|
|
stateConn, err = args.StateConn(connectionsCtx)
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
pool, err := storagePools.LoadByInstance(d.state, d)
|
|
if err != nil {
|
|
err := fmt.Errorf("Failed loading instance: %w", err)
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
clusterMove := args.ClusterMoveSourceName != ""
|
|
storageMove := args.StoragePool != ""
|
|
|
|
// The refresh argument passed to MigrationTypes() is always set to false here.
|
|
// The migration source/sender doesn't need to care whether or not it's doing a refresh as the migration
|
|
// sink/receiver will know this, and adjust the migration types accordingly.
|
|
// The same applies for clusterMove and storageMove, which are set to the most optimized defaults.
|
|
poolMigrationTypes := pool.MigrationTypes(storagePools.InstanceContentType(d), false, args.Snapshots, true, false)
|
|
if len(poolMigrationTypes) == 0 {
|
|
err := errors.New("No source migration types available")
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
// Convert the pool's migration type options to an offer header to target.
|
|
// Populate the Fs, ZfsFeatures and RsyncFeatures fields.
|
|
offerHeader := localMigration.TypesToHeader(poolMigrationTypes...)
|
|
|
|
// Offer to send index header.
|
|
indexHeaderVersion := localMigration.IndexHeaderVersion
|
|
offerHeader.IndexHeaderVersion = &indexHeaderVersion
|
|
|
|
// Add CRIU and predump info to source header.
|
|
maxDumpIterations := 0
|
|
if args.Live {
|
|
var offerUsePreDumps bool
|
|
offerUsePreDumps, maxDumpIterations = d.migrationSendCheckForPreDumpSupport()
|
|
offerHeader.Predump = proto.Bool(offerUsePreDumps)
|
|
offerHeader.Criu = migration.CRIUType_CRIU_RSYNC.Enum()
|
|
} else {
|
|
offerHeader.Predump = proto.Bool(false)
|
|
|
|
if d.IsRunning() {
|
|
// Indicate instance is running to target (can trigger MultiSync mode).
|
|
offerHeader.Criu = migration.CRIUType_NONE.Enum()
|
|
}
|
|
}
|
|
|
|
// Add idmap info to source header for containers.
|
|
idmapset, err := d.DiskIdmap()
|
|
if err != nil {
|
|
err := fmt.Errorf("Failed getting container disk idmap: %w", err)
|
|
op.Done(err)
|
|
return err
|
|
} else if idmapset != nil {
|
|
offerHeader.Idmap = make([]*migration.IDMapType, 0, len(idmapset.Entries))
|
|
for _, ctnIdmap := range idmapset.Entries {
|
|
idmap := migration.IDMapType{
|
|
Isuid: proto.Bool(ctnIdmap.IsUID),
|
|
Isgid: proto.Bool(ctnIdmap.IsGID),
|
|
Hostid: proto.Int32(int32(ctnIdmap.HostID)),
|
|
Nsid: proto.Int32(int32(ctnIdmap.NSID)),
|
|
Maprange: proto.Int32(int32(ctnIdmap.MapRange)),
|
|
}
|
|
|
|
offerHeader.Idmap = append(offerHeader.Idmap, &idmap)
|
|
}
|
|
}
|
|
|
|
srcConfig, err := pool.GenerateInstanceBackupConfig(d, args.Snapshots, d.op)
|
|
if err != nil {
|
|
err := fmt.Errorf("Failed generating instance migration config: %w", err)
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
// If we are copying snapshots, retrieve a list of snapshots from source volume.
|
|
if args.Snapshots {
|
|
offerHeader.SnapshotNames = make([]string, 0, len(srcConfig.Snapshots))
|
|
offerHeader.Snapshots = make([]*migration.Snapshot, 0, len(srcConfig.Snapshots))
|
|
|
|
for i := range srcConfig.Snapshots {
|
|
offerHeader.SnapshotNames = append(offerHeader.SnapshotNames, srcConfig.Snapshots[i].Name)
|
|
offerHeader.Snapshots = append(offerHeader.Snapshots, instance.SnapshotToProtobuf(srcConfig.Snapshots[i]))
|
|
}
|
|
}
|
|
|
|
// Send offer to target.
|
|
d.logger.Debug("Sending migration offer to target")
|
|
err = args.ControlSend(offerHeader)
|
|
if err != nil {
|
|
err := fmt.Errorf("Failed sending migration offer: %w", err)
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
// Receive response from target.
|
|
d.logger.Debug("Waiting for migration offer response from target")
|
|
respHeader := &migration.MigrationHeader{}
|
|
err = args.ControlReceive(respHeader)
|
|
if err != nil {
|
|
err := fmt.Errorf("Failed receiving migration offer response: %w", err)
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
d.logger.Debug("Got migration offer response from target")
|
|
|
|
// Negotiated migration types.
|
|
migrationTypes, err := localMigration.MatchTypes(respHeader, migration.MigrationFSType_RSYNC, poolMigrationTypes)
|
|
if err != nil {
|
|
err := fmt.Errorf("Failed to negotiate migration type: %w", err)
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
volSourceArgs := &localMigration.VolumeSourceArgs{
|
|
IndexHeaderVersion: respHeader.GetIndexHeaderVersion(), // Enable index header frame if supported.
|
|
Name: d.Name(),
|
|
MigrationType: migrationTypes[0],
|
|
Snapshots: offerHeader.SnapshotNames,
|
|
TrackProgress: true,
|
|
Refresh: respHeader.GetRefresh(),
|
|
AllowInconsistent: args.AllowInconsistent,
|
|
VolumeOnly: !args.Snapshots,
|
|
Info: &localMigration.Info{Config: srcConfig},
|
|
ClusterMove: clusterMove,
|
|
StorageMove: storageMove,
|
|
}
|
|
|
|
// Only send the snapshots that the target requests when refreshing.
|
|
if respHeader.GetRefresh() {
|
|
volSourceArgs.Snapshots = respHeader.GetSnapshotNames()
|
|
allSnapshots := volSourceArgs.Info.Config.VolumeSnapshots
|
|
|
|
// Ensure that only the requested snapshots are included in the migration index header.
|
|
volSourceArgs.Info.Config.VolumeSnapshots = make([]*api.StorageVolumeSnapshot, 0, len(volSourceArgs.Snapshots))
|
|
for i := range allSnapshots {
|
|
if slices.Contains(volSourceArgs.Snapshots, allSnapshots[i].Name) {
|
|
volSourceArgs.Info.Config.VolumeSnapshots = append(volSourceArgs.Info.Config.VolumeSnapshots, allSnapshots[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
// If s.live is true or Criu is set to CRIUType_NONE rather than nil, it indicates that the source instance
|
|
// is running, and if we are doing a non-optimized transfer (i.e using rsync or raw block transfer) then we
|
|
// should do a two stage transfer to minimize downtime.
|
|
instanceRunning := args.Live || (respHeader.Criu != nil && *respHeader.Criu == migration.CRIUType_NONE)
|
|
nonOptimizedMigration := volSourceArgs.MigrationType.FSType == migration.MigrationFSType_RSYNC || volSourceArgs.MigrationType.FSType == migration.MigrationFSType_BLOCK_AND_RSYNC
|
|
if instanceRunning && nonOptimizedMigration {
|
|
// Indicate this info to the storage driver so that it can alter its behaviour if needed.
|
|
volSourceArgs.MultiSync = true
|
|
}
|
|
|
|
g, ctx := errgroup.WithContext(context.Background())
|
|
|
|
// Start control connection monitor.
|
|
g.Go(func() error {
|
|
d.logger.Debug("Migrate send control monitor started")
|
|
defer d.logger.Debug("Migrate send control monitor finished")
|
|
|
|
controlResult := make(chan error, 1) // Buffered to allow go routine to end if no readers.
|
|
|
|
// This will read the result message from the target side and detect disconnections.
|
|
go func() {
|
|
resp := migration.MigrationControl{}
|
|
err := args.ControlReceive(&resp)
|
|
if err != nil {
|
|
err = fmt.Errorf("Error reading migration control target: %w", err)
|
|
} else if !resp.GetSuccess() {
|
|
err = fmt.Errorf("Error from migration control target: %s", resp.GetMessage())
|
|
}
|
|
|
|
controlResult <- err
|
|
}()
|
|
|
|
// End as soon as we get control message/disconnection from the target side or a local error.
|
|
select {
|
|
case <-ctx.Done():
|
|
err = ctx.Err()
|
|
case err = <-controlResult:
|
|
}
|
|
|
|
return err
|
|
})
|
|
|
|
// Start error monitoring routine, this will detect when an error is returned from the other routines,
|
|
// and if that happens it will disconnect the migration connections which will trigger the other routines
|
|
// to finish.
|
|
go func() {
|
|
<-ctx.Done()
|
|
args.Disconnect()
|
|
}()
|
|
|
|
restoreSuccess := make(chan bool, 1)
|
|
defer close(restoreSuccess)
|
|
|
|
// Don't defer close this one as its needed potentially after this function has ended.
|
|
dumpSuccess := make(chan error, 1)
|
|
|
|
g.Go(func() error {
|
|
d.logger.Debug("Migrate send transfer started")
|
|
defer d.logger.Debug("Migrate send transfer finished")
|
|
|
|
var err error
|
|
|
|
d.logger.Debug("Starting storage migration phase")
|
|
|
|
err = pool.MigrateInstance(d, filesystemConn, volSourceArgs, d.op)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
d.logger.Debug("Finished storage migration phase")
|
|
|
|
if args.Live {
|
|
d.logger.Debug("Starting live migration phase")
|
|
|
|
// Setup rsync options (used for CRIU state transfers).
|
|
rsyncBwlimit := pool.Driver().Config()["rsync.bwlimit"]
|
|
rsyncFeatures := respHeader.GetRsyncFeaturesSlice()
|
|
if !slices.Contains(rsyncFeatures, "bidirectional") {
|
|
// If no bi-directional support, assume 3.7 level.
|
|
// NOTE: Do NOT extend this list of arguments.
|
|
rsyncFeatures = []string{"xattrs", "delete", "compress"}
|
|
}
|
|
|
|
if respHeader.Criu == nil {
|
|
return errors.New("Got no CRIU socket type for live migration")
|
|
} else if *respHeader.Criu != migration.CRIUType_CRIU_RSYNC {
|
|
return fmt.Errorf("Formats other than criu rsync not understood (%q)", respHeader.Criu)
|
|
}
|
|
|
|
checkpointDir, err := os.MkdirTemp("", "incus_checkpoint_")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if liblxc.RuntimeLiblxcVersionAtLeast(liblxc.Version(), 2, 0, 4) {
|
|
// What happens below is slightly convoluted. Due to various complications
|
|
// with networking, there's no easy way for criu to exit and leave the
|
|
// container in a frozen state for us to somehow resume later.
|
|
// Instead, we use what criu calls an "action-script", which is basically a
|
|
// callback that lets us know when the dump is done. (Unfortunately, we
|
|
// can't pass arguments, just an executable path, so we write a custom
|
|
// action script with the real command we want to run.)
|
|
// This script then blocks until the migration operation either finishes
|
|
// successfully or fails, and exits 1 or 0, which causes criu to either
|
|
// leave the container running or kill it as we asked.
|
|
dumpDone := make(chan bool, 1)
|
|
actionScriptOpSecret, err := internalUtil.RandomHexString(32)
|
|
if err != nil {
|
|
_ = os.RemoveAll(checkpointDir)
|
|
return err
|
|
}
|
|
|
|
actionScriptOp, err := operations.OperationCreate(
|
|
d.state,
|
|
d.Project().Name,
|
|
operations.OperationClassWebsocket,
|
|
operationtype.InstanceLiveMigrate,
|
|
nil,
|
|
nil,
|
|
func(op *operations.Operation) error {
|
|
result := <-restoreSuccess
|
|
if !result {
|
|
return errors.New("restore failed, failing CRIU")
|
|
}
|
|
|
|
return nil
|
|
},
|
|
nil,
|
|
func(op *operations.Operation, r *http.Request, w http.ResponseWriter) error {
|
|
secret := r.FormValue("secret")
|
|
if secret == "" {
|
|
return errors.New("Missing action script secret")
|
|
}
|
|
|
|
if secret != actionScriptOpSecret {
|
|
return os.ErrPermission
|
|
}
|
|
|
|
c, err := ws.Upgrader.Upgrade(w, r, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dumpDone <- true
|
|
|
|
closeMsg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")
|
|
return c.WriteMessage(websocket.CloseMessage, closeMsg)
|
|
},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
_ = os.RemoveAll(checkpointDir)
|
|
return err
|
|
}
|
|
|
|
err = d.migrationSendWriteActionScript(checkpointDir, actionScriptOp.URL(), actionScriptOpSecret, d.state.OS.ExecPath)
|
|
if err != nil {
|
|
_ = os.RemoveAll(checkpointDir)
|
|
return err
|
|
}
|
|
|
|
preDumpCounter := 0
|
|
preDumpDir := ""
|
|
|
|
// Check if the other side knows about pre-dumping and the associated
|
|
// rsync protocol.
|
|
if respHeader.GetPredump() {
|
|
d.logger.Debug("The other side does support pre-copy")
|
|
final := false
|
|
for !final {
|
|
preDumpCounter++
|
|
if preDumpCounter < maxDumpIterations {
|
|
final = false
|
|
} else {
|
|
final = true
|
|
}
|
|
|
|
dumpDir := fmt.Sprintf("%03d", preDumpCounter)
|
|
loopArgs := preDumpLoopArgs{
|
|
stateConn: stateConn,
|
|
checkpointDir: checkpointDir,
|
|
bwlimit: rsyncBwlimit,
|
|
preDumpDir: preDumpDir,
|
|
dumpDir: dumpDir,
|
|
final: final,
|
|
rsyncFeatures: rsyncFeatures,
|
|
}
|
|
|
|
final, err = d.migrateSendPreDumpLoop(&loopArgs)
|
|
if err != nil {
|
|
_ = os.RemoveAll(checkpointDir)
|
|
return err
|
|
}
|
|
|
|
preDumpDir = fmt.Sprintf("%03d", preDumpCounter)
|
|
preDumpCounter++
|
|
}
|
|
} else {
|
|
d.logger.Debug("The other side does not support pre-copy")
|
|
}
|
|
|
|
err = actionScriptOp.Start()
|
|
if err != nil {
|
|
_ = os.RemoveAll(checkpointDir)
|
|
return err
|
|
}
|
|
|
|
go func() {
|
|
d.logger.Debug("Final CRIU dump started")
|
|
defer d.logger.Debug("Final CRIU dump stopped")
|
|
criuMigrationArgs := instance.CriuMigrationArgs{
|
|
Cmd: liblxc.MIGRATE_DUMP,
|
|
Stop: true,
|
|
ActionScript: true,
|
|
PreDumpDir: preDumpDir,
|
|
DumpDir: "final",
|
|
StateDir: checkpointDir,
|
|
Function: "migration",
|
|
}
|
|
|
|
// Do the final CRIU dump. This is needs no special handling if
|
|
// pre-dumps are used or not.
|
|
dumpSuccess <- d.migrate(&criuMigrationArgs)
|
|
_ = os.RemoveAll(checkpointDir)
|
|
}()
|
|
|
|
select {
|
|
// The checkpoint failed, let's just abort.
|
|
case err = <-dumpSuccess:
|
|
return err
|
|
// The dump finished, let's continue on to the restore.
|
|
case <-dumpDone:
|
|
d.logger.Debug("Dump finished, continuing with restore...")
|
|
}
|
|
} else {
|
|
d.logger.Debug("The version of liblxc is older than 2.0.4 and the live migration will probably fail")
|
|
defer func() { _ = os.RemoveAll(checkpointDir) }()
|
|
criuMigrationArgs := instance.CriuMigrationArgs{
|
|
Cmd: liblxc.MIGRATE_DUMP,
|
|
StateDir: checkpointDir,
|
|
Function: "migration",
|
|
Stop: true,
|
|
ActionScript: false,
|
|
DumpDir: "final",
|
|
PreDumpDir: "",
|
|
}
|
|
|
|
err = d.migrate(&criuMigrationArgs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// We do the transfer serially right now, but there's really no reason for us to;
|
|
// since we have separate websockets, we can do it in parallel if we wanted to.
|
|
// However assuming we're network bound, there's really no reason to do these in.
|
|
// parallel. In the future when we're using p.haul's protocol, it will make sense
|
|
// to do these in parallel.
|
|
ctName, _, _ := api.GetParentAndSnapshotName(d.Name())
|
|
err = rsync.Send(ctName, internalUtil.AddSlash(checkpointDir), stateConn, nil, rsyncFeatures, rsyncBwlimit, d.state.OS.ExecPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
d.logger.Debug("Finished live migration phase")
|
|
}
|
|
|
|
// Perform final sync if in multi sync mode.
|
|
if volSourceArgs.MultiSync {
|
|
d.logger.Debug("Starting final storage migration phase")
|
|
|
|
// Indicate to the storage driver we are doing final sync and because of this don't send
|
|
// snapshots as they don't need to have a final sync as not being modified.
|
|
volSourceArgs.FinalSync = true
|
|
volSourceArgs.Snapshots = nil
|
|
volSourceArgs.Info.Config.VolumeSnapshots = nil
|
|
|
|
err = pool.MigrateInstance(d, filesystemConn, volSourceArgs, d.op)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
d.logger.Debug("Finished final storage migration phase")
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
{
|
|
// Wait for routines to finish and collect first error.
|
|
err := g.Wait()
|
|
|
|
if args.Live {
|
|
restoreSuccess <- err == nil
|
|
|
|
if err == nil {
|
|
err := <-dumpSuccess
|
|
if err != nil {
|
|
d.logger.Error("Dump failed after successful restore", logger.Ctx{"err": err})
|
|
}
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
op.Done(err)
|
|
return err
|
|
}
|
|
|
|
op.Done(nil)
|
|
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceMigrated.Event(d, nil))
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
type preDumpLoopArgs struct {
|
|
stateConn io.ReadWriteCloser
|
|
checkpointDir string
|
|
bwlimit string
|
|
preDumpDir string
|
|
dumpDir string
|
|
final bool
|
|
rsyncFeatures []string
|
|
}
|
|
|
|
// migrateSendPreDumpLoop is the main logic behind the pre-copy migration.
|
|
// This function contains the actual pre-dump, the corresponding rsync transfer and it tells the outer loop to
|
|
// abort if the threshold of memory pages transferred by pre-dumping has been reached.
|
|
func (d *lxc) migrateSendPreDumpLoop(args *preDumpLoopArgs) (bool, error) {
|
|
// Do a CRIU pre-dump
|
|
criuMigrationArgs := instance.CriuMigrationArgs{
|
|
Cmd: liblxc.MIGRATE_PRE_DUMP,
|
|
Stop: false,
|
|
ActionScript: false,
|
|
PreDumpDir: args.preDumpDir,
|
|
DumpDir: args.dumpDir,
|
|
StateDir: args.checkpointDir,
|
|
Function: "migration",
|
|
}
|
|
|
|
d.logger.Debug("Doing another CRIU pre-dump", logger.Ctx{"preDumpDir": args.preDumpDir})
|
|
|
|
final := args.final
|
|
|
|
if d.Type() != instancetype.Container {
|
|
return false, errors.New("Instance is not container type")
|
|
}
|
|
|
|
err := d.migrate(&criuMigrationArgs)
|
|
if err != nil {
|
|
return final, fmt.Errorf("Failed sending instance: %w", err)
|
|
}
|
|
|
|
// Send the pre-dump.
|
|
ctName, _, _ := api.GetParentAndSnapshotName(d.Name())
|
|
err = rsync.Send(ctName, internalUtil.AddSlash(args.checkpointDir), args.stateConn, nil, args.rsyncFeatures, args.bwlimit, d.state.OS.ExecPath)
|
|
if err != nil {
|
|
return final, err
|
|
}
|
|
|
|
// The function readCriuStatsDump() reads the CRIU 'stats-dump' file
|
|
// in path and returns the pages_written, pages_skipped_parent, error.
|
|
readCriuStatsDump := func(path string) (uint64, uint64, error) {
|
|
// Get dump statistics with crit
|
|
dumpStats, err := crit.GetDumpStats(path)
|
|
if err != nil {
|
|
return 0, 0, fmt.Errorf("Failed to parse CRIU's 'stats-dump' file: %w", err)
|
|
}
|
|
|
|
return dumpStats.GetPagesWritten(), dumpStats.GetPagesSkippedParent(), nil
|
|
}
|
|
|
|
// Read the CRIU's 'stats-dump' file
|
|
dumpPath := internalUtil.AddSlash(args.checkpointDir)
|
|
dumpPath += internalUtil.AddSlash(args.dumpDir)
|
|
written, skippedParent, err := readCriuStatsDump(dumpPath)
|
|
if err != nil {
|
|
return final, err
|
|
}
|
|
|
|
totalPages := written + skippedParent
|
|
var percentageSkipped int
|
|
if totalPages > 0 {
|
|
percentageSkipped = int(100 - ((100 * written) / totalPages))
|
|
}
|
|
|
|
d.logger.Debug("CRIU pages", logger.Ctx{"pages": written, "skipped": skippedParent, "skippedPerc": percentageSkipped})
|
|
|
|
// threshold is the percentage of memory pages that needs
|
|
// to be pre-copied for the pre-copy migration to stop.
|
|
var threshold int
|
|
tmp := d.ExpandedConfig()["migration.incremental.memory.goal"]
|
|
if tmp != "" {
|
|
threshold, _ = strconv.Atoi(tmp)
|
|
} else {
|
|
// defaults to 70%
|
|
threshold = 70
|
|
}
|
|
|
|
if percentageSkipped > threshold {
|
|
d.logger.Debug("Memory pages skipped due to pre-copy is larger than threshold", logger.Ctx{"skippedPerc": percentageSkipped, "thresholdPerc": threshold})
|
|
d.logger.Debug("This was the last pre-dump; next dump is the final dump")
|
|
final = true
|
|
}
|
|
|
|
// If in pre-dump mode, the receiving side expects a message to know if this was the last pre-dump.
|
|
logger.Debug("Sending another CRIU pre-dump header")
|
|
sync := migration.MigrationSync{
|
|
FinalPreDump: proto.Bool(final),
|
|
}
|
|
|
|
data, err := proto.Marshal(&sync)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
_, err = args.stateConn.Write(data)
|
|
if err != nil {
|
|
return final, err
|
|
}
|
|
|
|
d.logger.Debug("Sending another CRIU pre-dump header done")
|
|
|
|
return final, nil
|
|
}
|
|
|
|
func (d *lxc) resetContainerDiskIdmap(srcIdmap *idmap.Set) error {
|
|
dstIdmap, err := d.DiskIdmap()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if dstIdmap == nil {
|
|
dstIdmap = &idmap.Set{}
|
|
}
|
|
|
|
if !srcIdmap.Equals(dstIdmap) {
|
|
jsonIdmap, err := srcIdmap.ToJSON()
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to encode ID map: %w", err)
|
|
}
|
|
|
|
d.logger.Debug("Setting new volatile.last_state.idmap from source instance", logger.Ctx{"sourceIdmap": srcIdmap})
|
|
err = d.VolatileSet(map[string]string{"volatile.last_state.idmap": jsonIdmap})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *lxc) MigrateReceive(args instance.MigrateReceiveArgs) error {
|
|
d.logger.Debug("Migration receive starting")
|
|
defer d.logger.Debug("Migration receive stopped")
|
|
|
|
// Wait for essential migration connections before negotiation.
|
|
connectionsCtx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
|
defer cancel()
|
|
|
|
filesystemConn, err := args.FilesystemConn(connectionsCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var stateConn io.ReadWriteCloser
|
|
if args.Live {
|
|
stateConn, err = args.StateConn(connectionsCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Receive offer from source.
|
|
d.logger.Debug("Waiting for migration offer from source")
|
|
offerHeader := &migration.MigrationHeader{}
|
|
err = args.ControlReceive(offerHeader)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed receiving migration offer from source: %w", err)
|
|
}
|
|
|
|
criuType := migration.CRIUType_CRIU_RSYNC.Enum()
|
|
if offerHeader.Criu != nil && *offerHeader.Criu == migration.CRIUType_NONE {
|
|
criuType = migration.CRIUType_NONE.Enum()
|
|
} else {
|
|
if !args.Live {
|
|
criuType = nil
|
|
}
|
|
}
|
|
|
|
// When doing a cluster same-name move we cannot load the storage pool using the instance's volume DB
|
|
// record because it may be associated to the wrong cluster member. Instead we ascertain the pool to load
|
|
// using the instance's root disk device.
|
|
if args.ClusterMoveSourceName == d.name {
|
|
_, rootDiskDevice, err := d.getRootDiskDevice()
|
|
if err != nil {
|
|
return fmt.Errorf("Failed getting root disk: %w", err)
|
|
}
|
|
|
|
if rootDiskDevice["pool"] == "" {
|
|
return errors.New("The instance's root device is missing the pool property")
|
|
}
|
|
|
|
// Initialize the storage pool cache.
|
|
d.storagePool, err = storagePools.LoadByName(d.state, rootDiskDevice["pool"])
|
|
if err != nil {
|
|
return fmt.Errorf("Failed loading storage pool: %w", err)
|
|
}
|
|
}
|
|
|
|
pool, err := storagePools.LoadByInstance(d.state, d)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// The source will never set Refresh in the offer header.
|
|
// However, to determine the correct migration type Refresh needs to be set.
|
|
offerHeader.Refresh = &args.Refresh
|
|
|
|
clusterMove := args.ClusterMoveSourceName != ""
|
|
storageMove := args.StoragePool != ""
|
|
|
|
// Extract the source's migration type and then match it against our pool's supported types and features.
|
|
// If a match is found the combined features list will be sent back to requester.
|
|
contentType := storagePools.InstanceContentType(d)
|
|
respTypes, err := localMigration.MatchTypes(offerHeader, storagePools.FallbackMigrationType(contentType), pool.MigrationTypes(contentType, args.Refresh, args.Snapshots, clusterMove, storageMove))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// The migration header to be sent back to source with our target options.
|
|
// Convert response type to response header and copy snapshot info into it.
|
|
respHeader := localMigration.TypesToHeader(respTypes...)
|
|
|
|
// Respond with our maximum supported header version if the requested version is higher than ours.
|
|
// Otherwise just return the requested header version to the source.
|
|
indexHeaderVersion := min(offerHeader.GetIndexHeaderVersion(), localMigration.IndexHeaderVersion)
|
|
|
|
respHeader.IndexHeaderVersion = &indexHeaderVersion
|
|
respHeader.SnapshotNames = offerHeader.SnapshotNames
|
|
respHeader.Snapshots = offerHeader.Snapshots
|
|
respHeader.Refresh = &args.Refresh
|
|
|
|
// Add CRIU info to response.
|
|
respHeader.Criu = criuType
|
|
|
|
if args.Refresh {
|
|
// Get the remote snapshots on the source.
|
|
sourceSnapshots := offerHeader.GetSnapshots()
|
|
sourceSnapshotComparable := make([]storagePools.ComparableSnapshot, 0, len(sourceSnapshots))
|
|
for _, sourceSnap := range sourceSnapshots {
|
|
sourceSnapshotComparable = append(sourceSnapshotComparable, storagePools.ComparableSnapshot{
|
|
Name: sourceSnap.GetName(),
|
|
CreationDate: time.Unix(sourceSnap.GetCreationDate(), 0),
|
|
})
|
|
}
|
|
|
|
// Get existing snapshots on the local target.
|
|
targetSnapshots, err := d.Snapshots()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
targetSnapshotsComparable := make([]storagePools.ComparableSnapshot, 0, len(targetSnapshots))
|
|
for _, targetSnap := range targetSnapshots {
|
|
_, targetSnapName, _ := api.GetParentAndSnapshotName(targetSnap.Name())
|
|
|
|
targetSnapshotsComparable = append(targetSnapshotsComparable, storagePools.ComparableSnapshot{
|
|
Name: targetSnapName,
|
|
CreationDate: targetSnap.CreationDate(),
|
|
})
|
|
}
|
|
|
|
// Compare the two sets.
|
|
syncSourceSnapshotIndexes, deleteTargetSnapshotIndexes := storagePools.CompareSnapshots(sourceSnapshotComparable, targetSnapshotsComparable, args.RefreshExcludeOlder)
|
|
|
|
// Delete the extra local snapshots first.
|
|
for _, deleteTargetSnapshotIndex := range deleteTargetSnapshotIndexes {
|
|
err := targetSnapshots[deleteTargetSnapshotIndex].Delete(true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Only request to send the snapshots that need updating.
|
|
syncSnapshotNames := make([]string, 0, len(syncSourceSnapshotIndexes))
|
|
syncSnapshots := make([]*migration.Snapshot, 0, len(syncSourceSnapshotIndexes))
|
|
for _, syncSourceSnapshotIndex := range syncSourceSnapshotIndexes {
|
|
syncSnapshotNames = append(syncSnapshotNames, sourceSnapshots[syncSourceSnapshotIndex].GetName())
|
|
syncSnapshots = append(syncSnapshots, sourceSnapshots[syncSourceSnapshotIndex])
|
|
}
|
|
|
|
respHeader.Snapshots = syncSnapshots
|
|
respHeader.SnapshotNames = syncSnapshotNames
|
|
offerHeader.Snapshots = syncSnapshots
|
|
offerHeader.SnapshotNames = syncSnapshotNames
|
|
}
|
|
|
|
if offerHeader.GetPredump() {
|
|
// If the other side wants pre-dump and if this side supports it, let's use it.
|
|
respHeader.Predump = proto.Bool(true)
|
|
} else {
|
|
respHeader.Predump = proto.Bool(false)
|
|
}
|
|
|
|
// Get rsync options from sender, these are passed into mySink function as part of
|
|
// MigrationSinkArgs below.
|
|
rsyncFeatures := respHeader.GetRsyncFeaturesSlice()
|
|
|
|
// Send response to source.
|
|
d.logger.Debug("Sending migration response to source")
|
|
err = args.ControlSend(respHeader)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed sending migration response to source: %w", err)
|
|
}
|
|
|
|
d.logger.Debug("Sent migration response to source")
|
|
|
|
srcIdmap := &idmap.Set{}
|
|
for _, idmapSet := range offerHeader.Idmap {
|
|
e := idmap.Entry{
|
|
IsUID: *idmapSet.Isuid,
|
|
IsGID: *idmapSet.Isgid,
|
|
NSID: int64(*idmapSet.Nsid),
|
|
HostID: int64(*idmapSet.Hostid),
|
|
MapRange: int64(*idmapSet.Maprange),
|
|
}
|
|
|
|
srcIdmap.Entries = append(srcIdmap.Entries, e)
|
|
}
|
|
|
|
reverter := revert.New()
|
|
defer reverter.Fail()
|
|
|
|
g, ctx := errgroup.WithContext(context.Background())
|
|
|
|
// Start control connection monitor.
|
|
g.Go(func() error {
|
|
d.logger.Debug("Migrate receive control monitor started")
|
|
defer d.logger.Debug("Migrate receive control monitor finished")
|
|
|
|
controlResult := make(chan error, 1) // Buffered to allow go routine to end if no readers.
|
|
|
|
// This will read the result message from the source side and detect disconnections.
|
|
go func() {
|
|
resp := migration.MigrationControl{}
|
|
err := args.ControlReceive(&resp)
|
|
if err != nil {
|
|
err = fmt.Errorf("Error reading migration control source: %w", err)
|
|
} else if !resp.GetSuccess() {
|
|
err = fmt.Errorf("Error from migration control source: %s", resp.GetMessage())
|
|
}
|
|
|
|
controlResult <- err
|
|
}()
|
|
|
|
// End as soon as we get control message/disconnection from the source side or a local error.
|
|
select {
|
|
case <-ctx.Done():
|
|
err = ctx.Err()
|
|
case err = <-controlResult:
|
|
}
|
|
|
|
return err
|
|
})
|
|
|
|
// Start error monitoring routine, this will detect when an error is returned from the other routines,
|
|
// and if that happens it will disconnect the migration connections which will trigger the other routines
|
|
// to finish.
|
|
go func() {
|
|
<-ctx.Done()
|
|
args.Disconnect()
|
|
}()
|
|
|
|
// Start filesystem transfer routine and initialize a channel that is closed when the routine finishes.
|
|
fsTransferDone := make(chan struct{})
|
|
g.Go(func() error {
|
|
defer close(fsTransferDone)
|
|
|
|
d.logger.Debug("Migrate receive filesystem transfer started")
|
|
defer d.logger.Debug("Migrate receive filesystem transfer finished")
|
|
|
|
var err error
|
|
|
|
// We do the fs receive in parallel so we don't have to reason about when to receive
|
|
// what. The sending side is smart enough to send the filesystem bits that it can
|
|
// before it seizes the container to start checkpointing, so the total transfer time
|
|
// will be minimized even if we're dumb here.
|
|
snapshots := []*migration.Snapshot{}
|
|
|
|
// Legacy: we only sent the snapshot names, so we just copy the container's
|
|
// config over, same as we used to do.
|
|
if len(offerHeader.SnapshotNames) != len(offerHeader.Snapshots) {
|
|
// Convert the instance to an api.InstanceSnapshot.
|
|
|
|
profileNames := make([]string, 0, len(d.Profiles()))
|
|
for _, p := range d.Profiles() {
|
|
profileNames = append(profileNames, p.Name)
|
|
}
|
|
|
|
architectureName, _ := osarch.ArchitectureName(d.Architecture())
|
|
apiInstSnap := &api.InstanceSnapshot{
|
|
InstanceSnapshotPut: api.InstanceSnapshotPut{
|
|
ExpiresAt: time.Time{},
|
|
},
|
|
Architecture: architectureName,
|
|
CreatedAt: d.CreationDate(),
|
|
LastUsedAt: d.LastUsedDate(),
|
|
Config: d.LocalConfig(),
|
|
Description: d.Description(),
|
|
Devices: d.LocalDevices().CloneNative(),
|
|
Ephemeral: d.IsEphemeral(),
|
|
Stateful: d.IsStateful(),
|
|
Profiles: profileNames,
|
|
}
|
|
|
|
for _, name := range offerHeader.SnapshotNames {
|
|
base := instance.SnapshotToProtobuf(apiInstSnap)
|
|
base.Name = &name
|
|
snapshots = append(snapshots, base)
|
|
}
|
|
} else {
|
|
snapshots = offerHeader.Snapshots
|
|
}
|
|
|
|
// Default to not expecting to receive the final rootfs sync.
|
|
sendFinalFsDelta := false
|
|
|
|
// If we are doing a stateful live transfer or the CRIU type indicates we
|
|
// are doing a stateless transfer with a running instance then we should
|
|
// expect the source to send us a final rootfs sync.
|
|
if args.Live {
|
|
sendFinalFsDelta = true
|
|
} else if criuType != nil && *criuType == migration.CRIUType_NONE {
|
|
sendFinalFsDelta = true
|
|
}
|
|
|
|
volTargetArgs := localMigration.VolumeTargetArgs{
|
|
IndexHeaderVersion: respHeader.GetIndexHeaderVersion(),
|
|
Name: d.Name(),
|
|
MigrationType: respTypes[0],
|
|
Refresh: args.Refresh, // Indicate to receiver volume should exist.
|
|
TrackProgress: true, // Use a progress tracker on receiver to get in-cluster progress information.
|
|
Live: sendFinalFsDelta, // Indicates we will get a final rootfs sync.
|
|
VolumeSize: offerHeader.GetVolumeSize(), // Block size setting override.
|
|
VolumeOnly: !args.Snapshots,
|
|
ClusterMoveSourceName: args.ClusterMoveSourceName,
|
|
StoragePool: args.StoragePool,
|
|
}
|
|
|
|
// At this point we have already figured out the parent container's root
|
|
// disk device so we can simply retrieve it from the expanded devices.
|
|
parentStoragePool := ""
|
|
parentExpandedDevices := d.ExpandedDevices()
|
|
parentLocalRootDiskDeviceKey, parentLocalRootDiskDevice, _ := internalInstance.GetRootDiskDevice(parentExpandedDevices.CloneNative())
|
|
if parentLocalRootDiskDeviceKey != "" {
|
|
parentStoragePool = parentLocalRootDiskDevice["pool"]
|
|
}
|
|
|
|
if parentStoragePool == "" {
|
|
return errors.New("Instance's root device is missing the pool property")
|
|
}
|
|
|
|
// A zero length Snapshots slice indicates volume only migration in
|
|
// VolumeTargetArgs. So if VolumeOnly was requested, do not populate them.
|
|
if args.Snapshots {
|
|
volTargetArgs.Snapshots = make([]*migration.Snapshot, 0, len(snapshots))
|
|
for _, snap := range snapshots {
|
|
volTargetArgs.Snapshots = append(volTargetArgs.Snapshots, &migration.Snapshot{Name: snap.Name})
|
|
|
|
// Only create snapshot instance DB records if not doing a cluster same-name move.
|
|
// As otherwise the DB records will already exist.
|
|
if args.ClusterMoveSourceName != d.name {
|
|
snapArgs, err := instance.SnapshotProtobufToInstanceArgs(d.state, d, snap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Ensure that snapshot and parent container have the same
|
|
// storage pool in their local root disk device. If the root
|
|
// disk device for the snapshot comes from a profile on the
|
|
// new instance as well we don't need to do anything.
|
|
if snapArgs.Devices != nil {
|
|
snapLocalRootDiskDeviceKey, _, _ := internalInstance.GetRootDiskDevice(snapArgs.Devices.CloneNative())
|
|
if snapLocalRootDiskDeviceKey != "" {
|
|
snapArgs.Devices[snapLocalRootDiskDeviceKey]["pool"] = parentStoragePool
|
|
}
|
|
}
|
|
|
|
// Create the snapshot instance.
|
|
_, snapInstOp, cleanup, err := instance.CreateInternal(d.state, *snapArgs, d.op, true, false)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed creating instance snapshot record %q: %w", snapArgs.Name, err)
|
|
}
|
|
|
|
reverter.Add(cleanup)
|
|
defer snapInstOp.Done(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
err = pool.CreateInstanceFromMigration(d, filesystemConn, volTargetArgs, d.op)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed creating instance on target: %w", err)
|
|
}
|
|
|
|
isRemoteClusterMove := clusterMove && pool.Driver().Info().Remote
|
|
|
|
// Only delete all instance volumes on error if the pool volume creation has succeeded to
|
|
// avoid deleting an existing conflicting volume.
|
|
if !volTargetArgs.Refresh && !isRemoteClusterMove {
|
|
reverter.Add(func() {
|
|
snapshots, _ := d.Snapshots()
|
|
snapshotCount := len(snapshots)
|
|
for k := range snapshots {
|
|
// Delete the snapshots in reverse order.
|
|
k = snapshotCount - 1 - k
|
|
_ = pool.DeleteInstanceSnapshot(snapshots[k], nil)
|
|
}
|
|
|
|
_ = pool.DeleteInstance(d, nil)
|
|
})
|
|
}
|
|
|
|
// For containers, the fs map of the source is sent as part of the migration
|
|
// stream, then at the end we need to record that map as last_state so that
|
|
// shifting can happen on startup if needed.
|
|
err = d.resetContainerDiskIdmap(srcIdmap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if args.ClusterMoveSourceName != d.name {
|
|
err = d.DeferTemplateApply(instance.TemplateTriggerCopy)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
// Start live state transfer routine (if required) and initialize a channel that is closed when the
|
|
// routine finishes. It is never closed if the routine is not started.
|
|
stateTransferDone := make(chan struct{})
|
|
if args.Live {
|
|
g.Go(func() error {
|
|
d.logger.Debug("Migrate receive state transfer started")
|
|
defer d.logger.Debug("Migrate receive state transfer finished")
|
|
|
|
defer close(stateTransferDone)
|
|
|
|
imagesDir, err := os.MkdirTemp("", "incus_restore_")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() { _ = os.RemoveAll(imagesDir) }()
|
|
|
|
sync := &migration.MigrationSync{
|
|
FinalPreDump: proto.Bool(false),
|
|
}
|
|
|
|
if respHeader.GetPredump() {
|
|
for !sync.GetFinalPreDump() {
|
|
d.logger.Debug("Waiting to receive pre-dump rsync")
|
|
|
|
// Transfer a CRIU pre-dump.
|
|
err = rsync.Recv(internalUtil.AddSlash(imagesDir), stateConn, nil, rsyncFeatures)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed receiving pre-dump rsync: %w", err)
|
|
}
|
|
|
|
d.logger.Debug("Done receiving pre-dump rsync")
|
|
|
|
d.logger.Debug("Waiting to receive pre-dump header")
|
|
|
|
// We can't use io.ReadAll here because sender doesn't call Close() to
|
|
// send the frame end indicator after writing the pre-dump header.
|
|
// So define a small buffer sufficient to fit migration.MigrationSync and
|
|
// then read what we have into it.
|
|
buf := make([]byte, 128)
|
|
n, err := stateConn.Read(buf)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed receiving pre-dump header: %w", err)
|
|
}
|
|
|
|
err = proto.Unmarshal(buf[:n], sync)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed unmarshalling pre-dump header: %w (%v)", err, string(buf))
|
|
}
|
|
|
|
d.logger.Debug("Done receiving pre-dump header")
|
|
}
|
|
}
|
|
|
|
// Final CRIU dump.
|
|
d.logger.Debug("About to receive final dump rsync")
|
|
err = rsync.Recv(internalUtil.AddSlash(imagesDir), stateConn, nil, rsyncFeatures)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed receiving final dump rsync: %w", err)
|
|
}
|
|
|
|
d.logger.Debug("Done receiving final dump rsync")
|
|
|
|
// Wait until filesystem transfer is done before starting final state sync and restore.
|
|
<-fsTransferDone
|
|
|
|
// But only proceed if no errors have occurred thus far.
|
|
err = ctx.Err()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
criuMigrationArgs := instance.CriuMigrationArgs{
|
|
Cmd: liblxc.MIGRATE_RESTORE,
|
|
StateDir: imagesDir,
|
|
Function: "migration",
|
|
Stop: false,
|
|
ActionScript: false,
|
|
DumpDir: "final",
|
|
PreDumpDir: "",
|
|
}
|
|
|
|
// Currently we only do a single CRIU pre-dump so we can hardcode "final"
|
|
// here since we know that "final" is the folder for CRIU's final dump.
|
|
err = d.migrate(&criuMigrationArgs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
{
|
|
// Wait until the filesystem transfer and state transfer routines have finished.
|
|
<-fsTransferDone
|
|
if args.Live {
|
|
<-stateTransferDone
|
|
}
|
|
|
|
// If context is cancelled by this stage, then an error has occurred.
|
|
// Wait for all routines to finish and collect the first error that occurred.
|
|
if ctx.Err() != nil {
|
|
err := g.Wait()
|
|
|
|
// Send failure response to source.
|
|
msg := migration.MigrationControl{
|
|
Success: proto.Bool(err == nil),
|
|
}
|
|
|
|
if err != nil {
|
|
msg.Message = proto.String(err.Error())
|
|
}
|
|
|
|
d.logger.Debug("Sending migration failure response to source", logger.Ctx{"err": err})
|
|
sendErr := args.ControlSend(&msg)
|
|
if sendErr != nil {
|
|
d.logger.Warn("Failed sending migration failure to source", logger.Ctx{"err": sendErr})
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// Send success response to source to control as nothing has gone wrong so far.
|
|
msg := migration.MigrationControl{
|
|
Success: proto.Bool(true),
|
|
}
|
|
|
|
d.logger.Debug("Sending migration success response to source", logger.Ctx{"success": msg.GetSuccess()})
|
|
err := args.ControlSend(&msg)
|
|
if err != nil {
|
|
d.logger.Warn("Failed sending migration success to source", logger.Ctx{"err": err})
|
|
return fmt.Errorf("Failed sending migration success to source: %w", err)
|
|
}
|
|
|
|
// Wait for all routines to finish (in this case it will be the control monitor) but do
|
|
// not collect the error, as it will just be a disconnect error from the source.
|
|
_ = g.Wait()
|
|
|
|
reverter.Success()
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Migrate migrates the instance to another node.
|
|
func (d *lxc) migrate(args *instance.CriuMigrationArgs) error {
|
|
ctxMap := logger.Ctx{
|
|
"created": d.creationDate,
|
|
"ephemeral": d.ephemeral,
|
|
"used": d.lastUsedDate,
|
|
"statedir": args.StateDir,
|
|
"actionscript": args.ActionScript,
|
|
"predumpdir": args.PreDumpDir,
|
|
"features": args.Features,
|
|
"stop": args.Stop,
|
|
}
|
|
|
|
_, err := exec.LookPath("criu")
|
|
if err != nil {
|
|
return localMigration.ErrNoLiveMigration
|
|
}
|
|
|
|
d.logger.Info("Migrating container", ctxMap)
|
|
|
|
prettyCmd := ""
|
|
switch args.Cmd {
|
|
case liblxc.MIGRATE_PRE_DUMP:
|
|
prettyCmd = "pre-dump"
|
|
case liblxc.MIGRATE_DUMP:
|
|
prettyCmd = "dump"
|
|
case liblxc.MIGRATE_RESTORE:
|
|
prettyCmd = "restore"
|
|
default:
|
|
prettyCmd = "unknown"
|
|
d.logger.Warn("Unknown migrate call", logger.Ctx{"cmd": args.Cmd})
|
|
}
|
|
|
|
pool, err := d.getStoragePool()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
preservesInodes := pool.Driver().Info().PreservesInodes
|
|
|
|
/* This feature was only added in 2.0.1, let's not ask for it
|
|
* before then or migrations will fail.
|
|
*/
|
|
if !liblxc.RuntimeLiblxcVersionAtLeast(liblxc.Version(), 2, 0, 1) {
|
|
preservesInodes = false
|
|
}
|
|
|
|
finalStateDir := args.StateDir
|
|
var migrateErr error
|
|
|
|
/* For restore, we need an extra fork so that we daemonize the monitor
|
|
* instead of having it be a child. So let's hijack the command
|
|
* here and do the extra fork.
|
|
*/
|
|
if args.Cmd == liblxc.MIGRATE_RESTORE {
|
|
// Check that we're not already running.
|
|
if d.IsRunning() {
|
|
return errors.New("The container is already running")
|
|
}
|
|
|
|
// Run the shared start code.
|
|
configPath, postStartHooks, err := d.startCommon()
|
|
if err != nil {
|
|
if args.Op != nil {
|
|
args.Op.Done(err)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
/*
|
|
* For unprivileged containers we need to shift the
|
|
* perms on the images images so that they can be
|
|
* opened by the process after it is in its user
|
|
* namespace.
|
|
*/
|
|
idmapset, err := d.CurrentIdmap()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if idmapset != nil {
|
|
storageType, err := d.getStorageType()
|
|
if err != nil {
|
|
return fmt.Errorf("Storage type: %w", err)
|
|
}
|
|
|
|
if storageType == "zfs" {
|
|
err = idmapset.ShiftPath(args.StateDir, storageDrivers.ShiftZFSSkipper)
|
|
} else if storageType == "btrfs" {
|
|
err = storageDrivers.ShiftBtrfsRootfs(args.StateDir, idmapset)
|
|
} else {
|
|
err = idmapset.ShiftPath(args.StateDir, nil)
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if args.DumpDir != "" {
|
|
finalStateDir = fmt.Sprintf("%s/%s", args.StateDir, args.DumpDir)
|
|
}
|
|
|
|
_, migrateErr = subprocess.RunCommand(
|
|
d.state.OS.ExecPath,
|
|
"forkmigrate",
|
|
d.name,
|
|
d.state.OS.LxcPath,
|
|
configPath,
|
|
finalStateDir,
|
|
fmt.Sprintf("%v", preservesInodes),
|
|
)
|
|
|
|
if migrateErr == nil {
|
|
// Run any post start hooks.
|
|
err = d.runHooks(postStartHooks)
|
|
if err != nil {
|
|
if args.Op != nil {
|
|
args.Op.Done(err) // Must come before Stop() otherwise stop will not proceed.
|
|
}
|
|
|
|
// Attempt to stop container.
|
|
_ = d.Stop(false)
|
|
|
|
return err
|
|
}
|
|
}
|
|
} else {
|
|
// Load the go-lxc struct
|
|
var cc *liblxc.Container
|
|
if d.expandedConfig["raw.lxc"] != "" {
|
|
cc, err = d.initLXC(true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = d.loadRawLXCConfig(cc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
cc, err = d.initLXC(false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
script := ""
|
|
if args.ActionScript {
|
|
script = filepath.Join(args.StateDir, "action.sh")
|
|
}
|
|
|
|
if args.DumpDir != "" {
|
|
finalStateDir = fmt.Sprintf("%s/%s", args.StateDir, args.DumpDir)
|
|
}
|
|
|
|
// TODO: make this configurable? Ultimately I think we don't
|
|
// want to do that; what we really want to do is have "modes"
|
|
// of criu operation where one is "make this succeed" and the
|
|
// other is "make this fast". Anyway, for now, let's choose a
|
|
// really big size so it almost always succeeds, even if it is
|
|
// slow.
|
|
ghostLimit := uint64(256 * 1024 * 1024)
|
|
|
|
opts := liblxc.MigrateOptions{
|
|
Stop: args.Stop,
|
|
Directory: finalStateDir,
|
|
Verbose: true,
|
|
PreservesInodes: preservesInodes,
|
|
ActionScript: script,
|
|
GhostLimit: ghostLimit,
|
|
}
|
|
|
|
if args.PreDumpDir != "" {
|
|
opts.PredumpDir = fmt.Sprintf("../%s", args.PreDumpDir)
|
|
}
|
|
|
|
if !d.IsRunning() {
|
|
// otherwise the migration will needlessly fail
|
|
args.Stop = false
|
|
}
|
|
|
|
migrateErr = cc.Migrate(args.Cmd, opts)
|
|
}
|
|
|
|
collectErr := collectCRIULogFile(d, finalStateDir, args.Function, prettyCmd)
|
|
if collectErr != nil {
|
|
d.logger.Error("Error collecting checkpoint log file", logger.Ctx{"err": collectErr})
|
|
}
|
|
|
|
if migrateErr != nil {
|
|
log, err2 := getCRIULogErrors(finalStateDir, prettyCmd)
|
|
if err2 == nil {
|
|
d.logger.Warn("Failed migrating container", ctxMap)
|
|
migrateErr = fmt.Errorf("%s %s failed\n%s", args.Function, prettyCmd, log)
|
|
}
|
|
|
|
return migrateErr
|
|
}
|
|
|
|
d.logger.Info("Migrated container", ctxMap)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *lxc) templateApplyNow(trigger instance.TemplateTrigger) error {
|
|
// If there's no metadata, just return
|
|
fname := filepath.Join(d.Path(), "metadata.yaml")
|
|
if !util.PathExists(fname) {
|
|
return nil
|
|
}
|
|
|
|
// Parse the metadata
|
|
content, err := os.ReadFile(fname)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to read metadata: %w", err)
|
|
}
|
|
|
|
metadata := &api.ImageMetadata{}
|
|
err = yaml.Unmarshal(content, &metadata)
|
|
if err != nil {
|
|
return fmt.Errorf("Could not parse %s: %w", fname, err)
|
|
}
|
|
|
|
// Find rootUID and rootGID
|
|
idmapset, err := d.DiskIdmap()
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to set ID map: %w", err)
|
|
}
|
|
|
|
rootUID := int64(0)
|
|
rootGID := int64(0)
|
|
|
|
// Get the right uid and gid for the container
|
|
if idmapset != nil {
|
|
rootUID, rootGID = idmapset.ShiftIntoNS(0, 0)
|
|
}
|
|
|
|
// Figure out the container architecture
|
|
arch, err := osarch.ArchitectureName(d.architecture)
|
|
if err != nil {
|
|
arch, err = osarch.ArchitectureName(d.state.OS.Architectures[0])
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to detect system architecture: %w", err)
|
|
}
|
|
}
|
|
|
|
// Generate the container metadata
|
|
containerMeta := make(map[string]string)
|
|
containerMeta["name"] = d.name
|
|
containerMeta["type"] = "container"
|
|
containerMeta["architecture"] = arch
|
|
|
|
if d.ephemeral {
|
|
containerMeta["ephemeral"] = "true"
|
|
} else {
|
|
containerMeta["ephemeral"] = "false"
|
|
}
|
|
|
|
if d.IsPrivileged() {
|
|
containerMeta["privileged"] = "true"
|
|
} else {
|
|
containerMeta["privileged"] = "false"
|
|
}
|
|
|
|
// Setup security check.
|
|
rootfsPath, err := os.OpenFile(d.RootfsPath(), unix.O_PATH, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to open instance rootfs path: %w", err)
|
|
}
|
|
|
|
defer func() { _ = rootfsPath.Close() }()
|
|
|
|
checkBeneath := func(targetPath string) error {
|
|
fd, err := unix.Openat2(int(rootfsPath.Fd()), targetPath, &unix.OpenHow{
|
|
Flags: unix.O_PATH | unix.O_CLOEXEC,
|
|
Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_MAGICLINKS,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, unix.EXDEV) {
|
|
return errors.New("Template is attempting access to path outside of container")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
_ = unix.Close(fd)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Go through the templates
|
|
for tplPath, tpl := range metadata.Templates {
|
|
err = func(tplPath string, tpl *api.ImageMetadataTemplate) error {
|
|
var w *os.File
|
|
|
|
// Check if the template should be applied now
|
|
found := slices.Contains(tpl.When, string(trigger))
|
|
|
|
if !found {
|
|
return nil
|
|
}
|
|
|
|
// Perform some security checks.
|
|
relPath := strings.TrimLeft(tplPath, "/")
|
|
|
|
err = checkBeneath(relPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if filepath.Base(tpl.Template) != tpl.Template {
|
|
return errors.New("Template path is attempting to read outside of template directory")
|
|
}
|
|
|
|
tplDirStat, err := os.Lstat(d.TemplatesPath())
|
|
if err != nil {
|
|
return fmt.Errorf("Couldn't access template directory: %w", err)
|
|
}
|
|
|
|
if !tplDirStat.IsDir() {
|
|
return errors.New("Template directory isn't a regular directory")
|
|
}
|
|
|
|
tplFileStat, err := os.Lstat(filepath.Join(d.TemplatesPath(), tpl.Template))
|
|
if err != nil {
|
|
return fmt.Errorf("Couldn't access template file: %w", err)
|
|
}
|
|
|
|
if tplFileStat.Mode()&os.ModeSymlink == os.ModeSymlink {
|
|
return errors.New("Template file is a symlink")
|
|
}
|
|
|
|
// Open the file to template, create if needed
|
|
fullpath := filepath.Join(d.RootfsPath(), relPath)
|
|
if util.PathExists(fullpath) {
|
|
if tpl.CreateOnly {
|
|
return nil
|
|
}
|
|
|
|
// Open the existing file
|
|
w, err = os.Create(fullpath)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to create template file: %w", err)
|
|
}
|
|
} else {
|
|
// UID and GID
|
|
fileUID := int64(0)
|
|
fileGID := int64(0)
|
|
|
|
if tpl.UID != "" {
|
|
id, err := strconv.ParseInt(tpl.UID, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("Bad file UID %q for %q: %w", tpl.UID, tplPath, err)
|
|
}
|
|
|
|
fileUID = id
|
|
}
|
|
|
|
if tpl.GID != "" {
|
|
id, err := strconv.ParseInt(tpl.GID, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("Bad file GID %q for %q: %w", tpl.GID, tplPath, err)
|
|
}
|
|
|
|
fileGID = id
|
|
}
|
|
|
|
if idmapset != nil {
|
|
fileUID, fileGID = idmapset.ShiftIntoNS(fileUID, fileGID)
|
|
}
|
|
|
|
// Mode
|
|
fileMode := fs.FileMode(0o644)
|
|
if tpl.Mode != "" {
|
|
if len(tpl.Mode) == 3 {
|
|
tpl.Mode = fmt.Sprintf("0%s", tpl.Mode)
|
|
}
|
|
|
|
mode, err := strconv.ParseInt(tpl.Mode, 0, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("Bad mode %q for %q: %w", tpl.Mode, tplPath, err)
|
|
}
|
|
|
|
fileMode = os.FileMode(mode) & os.ModePerm
|
|
}
|
|
|
|
// Create the directories leading to the file
|
|
err = internalUtil.MkdirAllOwner(path.Dir(fullpath), 0o755, int(rootUID), int(rootGID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create the file itself
|
|
w, err = os.Create(fullpath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Fix ownership and mode
|
|
err = w.Chown(int(fileUID), int(fileGID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = w.Chmod(fileMode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
defer func() { _ = w.Close() }()
|
|
|
|
// Read the template
|
|
tplString, err := os.ReadFile(filepath.Join(d.TemplatesPath(), tpl.Template))
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to read template file: %w", err)
|
|
}
|
|
|
|
configGet := func(confKey, confDefault *pongo2.Value) *pongo2.Value {
|
|
val, ok := d.expandedConfig[confKey.String()]
|
|
if !ok {
|
|
return confDefault
|
|
}
|
|
|
|
return pongo2.AsValue(strings.TrimRight(val, "\r\n"))
|
|
}
|
|
|
|
err = internalUtil.RenderTemplateFile(w, string(tplString), pongo2.Context{
|
|
"trigger": trigger,
|
|
"path": tplPath,
|
|
"container": containerMeta,
|
|
"instance": containerMeta,
|
|
"config": d.expandedConfig,
|
|
"devices": d.expandedDevices,
|
|
"properties": tpl.Properties,
|
|
"config_get": configGet,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to render template: %w", err)
|
|
}
|
|
|
|
return w.Close()
|
|
}(tplPath, tpl)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *lxc) inheritInitPidFd() *os.File {
|
|
if d.state.OS.PidFds {
|
|
pidFdFile, err := d.InitPidFd()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
return pidFdFile
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// FileSFTPConn returns a connection to the forkfile handler.
|
|
func (d *lxc) FileSFTPConn() (net.Conn, error) {
|
|
// Lock to avoid concurrent spawning.
|
|
spawnUnlock, err := locking.Lock(context.TODO(), fmt.Sprintf("forkfile_%d", d.id))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer spawnUnlock()
|
|
|
|
// Create any missing directories in case the instance has never been started before.
|
|
err = os.MkdirAll(d.RunPath(), 0o700)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Trickery to handle paths > 108 chars.
|
|
dirFile, err := os.Open(d.RunPath())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer func() { _ = dirFile.Close() }()
|
|
|
|
forkfileAddr, err := net.ResolveUnixAddr("unix", fmt.Sprintf("/proc/self/fd/%d/forkfile.sock", dirFile.Fd()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Attempt to connect on existing socket.
|
|
forkfilePath := filepath.Join(d.RunPath(), "forkfile.sock")
|
|
forkfileConn, err := net.DialUnix("unix", nil, forkfileAddr)
|
|
if err == nil {
|
|
// Found an existing server.
|
|
return forkfileConn, nil
|
|
}
|
|
|
|
// Check for ongoing operations (that may involve shifting or replacing the root volume) so as to avoid
|
|
// allowing SFTP access while the container's filesystem setup is in flux.
|
|
// If there is an update operation ongoing and the instance is running then do not wait for the operation
|
|
// to complete before continuing as it is possible that a disk device is being removed that requires SFTP
|
|
// to clean up the path inside the container. Also it is not possible to be shifting/replacing the root
|
|
// volume when the instance is running, so there should be no reason to wait for the operation to finish.
|
|
op := operationlock.Get(d.Project().Name, d.Name())
|
|
if op.Action() != operationlock.ActionUpdate || !d.IsRunning() {
|
|
_ = op.Wait(context.Background())
|
|
}
|
|
|
|
// Setup reverter.
|
|
reverter := revert.New()
|
|
defer reverter.Fail()
|
|
|
|
// Create the listener.
|
|
_ = os.Remove(forkfilePath)
|
|
forkfileListener, err := net.ListenUnix("unix", forkfileAddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
reverter.Add(func() {
|
|
_ = forkfileListener.Close()
|
|
_ = os.Remove(forkfilePath)
|
|
})
|
|
|
|
// Spawn forkfile in a Go routine.
|
|
chReady := make(chan error)
|
|
go func() {
|
|
// Lock to avoid concurrent running forkfile.
|
|
runUnlock, err := locking.Lock(context.TODO(), d.forkfileRunningLockName())
|
|
if err != nil {
|
|
chReady <- err
|
|
return
|
|
}
|
|
|
|
defer runUnlock()
|
|
|
|
// Mount the filesystem if needed.
|
|
if !d.IsRunning() {
|
|
// Mount the root filesystem if required.
|
|
_, err := d.mount()
|
|
if err != nil {
|
|
chReady <- err
|
|
return
|
|
}
|
|
|
|
defer func() { _ = d.unmount() }()
|
|
}
|
|
|
|
// Start building the command.
|
|
args := []string{
|
|
d.state.OS.ExecPath,
|
|
"forkfile",
|
|
"--",
|
|
}
|
|
|
|
extraFiles := []*os.File{}
|
|
|
|
// Get the listener file.
|
|
forkfileFile, err := forkfileListener.File()
|
|
if err != nil {
|
|
chReady <- err
|
|
return
|
|
}
|
|
|
|
defer func() { _ = forkfileFile.Close() }()
|
|
|
|
args = append(args, "3")
|
|
extraFiles = append(extraFiles, forkfileFile)
|
|
|
|
// Get the rootfs.
|
|
rootfsFile, err := os.Open(d.RootfsPath())
|
|
if err != nil {
|
|
chReady <- err
|
|
return
|
|
}
|
|
|
|
defer func() { _ = rootfsFile.Close() }()
|
|
|
|
args = append(args, "4")
|
|
extraFiles = append(extraFiles, rootfsFile)
|
|
|
|
// Get the pidfd.
|
|
pidFd := d.inheritInitPidFd()
|
|
if pidFd != nil {
|
|
defer func() { _ = pidFd.Close() }()
|
|
args = append(args, "5")
|
|
extraFiles = append(extraFiles, pidFd)
|
|
} else {
|
|
args = append(args, "-1")
|
|
}
|
|
|
|
// Finalize the args.
|
|
args = append(args, fmt.Sprintf("%d", d.InitPID()))
|
|
|
|
// Prepare sftp server.
|
|
forkfile := exec.Cmd{
|
|
Path: d.state.OS.ExecPath,
|
|
Args: args,
|
|
ExtraFiles: extraFiles,
|
|
}
|
|
|
|
var stderr bytes.Buffer
|
|
forkfile.Stderr = &stderr
|
|
|
|
if !d.IsRunning() {
|
|
// Get the disk idmap.
|
|
idmapset, err := d.DiskIdmap()
|
|
if err != nil {
|
|
chReady <- err
|
|
return
|
|
}
|
|
|
|
if idmapset != nil {
|
|
forkfile.SysProcAttr = &syscall.SysProcAttr{
|
|
Cloneflags: syscall.CLONE_NEWUSER,
|
|
Credential: &syscall.Credential{
|
|
Uid: uint32(0),
|
|
Gid: uint32(0),
|
|
},
|
|
UidMappings: idmapset.ToUIDMappings(),
|
|
GidMappings: idmapset.ToGIDMappings(),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Start the server.
|
|
err = forkfile.Start()
|
|
if err != nil {
|
|
chReady <- fmt.Errorf("Failed to run forkfile: %w: %s", err, strings.TrimSpace(stderr.String()))
|
|
return
|
|
}
|
|
|
|
// Write PID file.
|
|
pidFile := filepath.Join(d.RunPath(), "forkfile.pid")
|
|
err = os.WriteFile(pidFile, fmt.Appendf(nil, "%d\n", forkfile.Process.Pid), 0o600)
|
|
if err != nil {
|
|
chReady <- fmt.Errorf("Failed to write forkfile PID: %w", err)
|
|
return
|
|
}
|
|
|
|
// Close the listener and delete the socket immediately after forkfile exits to avoid clients
|
|
// thinking a listener is available while other deferred calls are being processed.
|
|
defer func() {
|
|
_ = forkfileListener.Close()
|
|
_ = os.Remove(forkfilePath)
|
|
_ = os.Remove(pidFile)
|
|
}()
|
|
|
|
// Indicate the process was spawned without error.
|
|
close(chReady)
|
|
|
|
// Wait for completion.
|
|
err = forkfile.Wait()
|
|
if err != nil {
|
|
d.logger.Error("SFTP server stopped with error", logger.Ctx{"err": err, "stderr": strings.TrimSpace(stderr.String())})
|
|
return
|
|
}
|
|
}()
|
|
|
|
// Wait for forkfile to have been spawned.
|
|
err = <-chReady
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Connect to the new server.
|
|
forkfileConn, err = net.DialUnix("unix", nil, forkfileAddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// All done.
|
|
reverter.Success()
|
|
|
|
return forkfileConn, nil
|
|
}
|
|
|
|
// FileSFTP returns an SFTP connection to the forkfile handler.
|
|
func (d *lxc) FileSFTP() (*sftp.Client, error) {
|
|
// Connect to the forkfile daemon.
|
|
conn, err := d.FileSFTPConn()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get a SFTP client.
|
|
client, err := sftp.NewClientPipe(conn, conn)
|
|
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
|
|
}
|
|
|
|
// stopForkFile attempts to send SIGTERM (if force is true) or SIGINT to forkfile then waits for it to exit.
|
|
func (d *lxc) stopForkfile(force bool) {
|
|
// Make sure that when the function exits, no forkfile is running by acquiring the lock (which indicates
|
|
// that forkfile isn't running and holding the lock) and then releasing it.
|
|
defer func() {
|
|
unlock, err := locking.Lock(context.TODO(), d.forkfileRunningLockName())
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
unlock()
|
|
}()
|
|
|
|
content, err := os.ReadFile(filepath.Join(d.RunPath(), "forkfile.pid"))
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
pid, err := strconv.ParseInt(strings.TrimSpace(string(content)), 10, 64)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
d.logger.Debug("Stopping forkfile", logger.Ctx{"pid": pid, "force": force})
|
|
|
|
if force {
|
|
// Forcefully kill the running process.
|
|
_ = unix.Kill(int(pid), unix.SIGTERM)
|
|
} else {
|
|
// Try to send SIGINT to forkfile to indicate it should not accept any new connection.
|
|
_ = unix.Kill(int(pid), unix.SIGINT)
|
|
}
|
|
}
|
|
|
|
// Console attaches to the instance console.
|
|
func (d *lxc) Console(protocol string) (*os.File, chan error, error) {
|
|
if protocol != instance.ConsoleTypeConsole {
|
|
return nil, nil, fmt.Errorf("Container instances don't support %q output", protocol)
|
|
}
|
|
|
|
chDisconnect := make(chan error, 1)
|
|
|
|
args := []string{
|
|
d.state.OS.ExecPath,
|
|
"forkconsole",
|
|
project.Instance(d.Project().Name, d.Name()),
|
|
d.state.OS.LxcPath,
|
|
filepath.Join(d.RunPath(), "lxc.conf"),
|
|
"tty=0",
|
|
"escape=-1",
|
|
}
|
|
|
|
idmapset, err := d.CurrentIdmap()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
var rootUID, rootGID int64
|
|
if idmapset != nil {
|
|
rootUID, rootGID = idmapset.ShiftIntoNS(0, 0)
|
|
}
|
|
|
|
// Create a PTS pair.
|
|
ptx, pty, err := linux.OpenPty(rootUID, rootGID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Switch the console file descriptor into raw mode.
|
|
_, err = termios.MakeRaw(int(ptx.Fd()))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
cmd := exec.Cmd{}
|
|
cmd.Path = d.state.OS.ExecPath
|
|
cmd.Args = args
|
|
cmd.Stdin = pty
|
|
cmd.Stdout = pty
|
|
cmd.Stderr = pty
|
|
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
go func() {
|
|
err = cmd.Wait()
|
|
_ = ptx.Close()
|
|
_ = pty.Close()
|
|
}()
|
|
|
|
go func() {
|
|
<-chDisconnect
|
|
_ = cmd.Process.Kill()
|
|
}()
|
|
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceConsole.Event(d, logger.Ctx{"type": instance.ConsoleTypeConsole}))
|
|
|
|
return ptx, chDisconnect, nil
|
|
}
|
|
|
|
// ConsoleLog returns console log.
|
|
func (d *lxc) ConsoleLog(opts liblxc.ConsoleLogOptions) (string, error) {
|
|
cc, err := d.initLXC(false)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
msg, err := cc.ConsoleLog(opts)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if opts.ClearLog {
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceConsoleReset.Event(d, nil))
|
|
} else if opts.ReadLog && opts.WriteToLogFile {
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceConsoleRetrieved.Event(d, nil))
|
|
}
|
|
|
|
return string(msg), nil
|
|
}
|
|
|
|
// Exec executes a command inside the instance.
|
|
func (d *lxc) Exec(req api.InstanceExecPost, stdin *os.File, stdout *os.File, stderr *os.File) (instance.Cmd, error) {
|
|
// Generate the LXC config if missing.
|
|
configPath := filepath.Join(d.RunPath(), "lxc.conf")
|
|
if !util.PathExists(configPath) {
|
|
cc, err := d.initLXC(true)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Load go-lxc struct: %w", err)
|
|
}
|
|
|
|
err = cc.SaveConfigFile(configPath)
|
|
if err != nil {
|
|
_ = os.Remove(configPath)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Prepare the environment
|
|
envSlice := []string{}
|
|
|
|
for k, v := range req.Environment {
|
|
envSlice = append(envSlice, fmt.Sprintf("%s=%s", k, v))
|
|
}
|
|
|
|
// Setup logfile
|
|
logPath := filepath.Join(d.LogPath(), "forkexec.log")
|
|
logFile, err := os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_SYNC, 0o644)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer func() { _ = logFile.Close() }()
|
|
|
|
// Prepare the subcommand
|
|
cname := project.Instance(d.Project().Name, d.Name())
|
|
args := []string{
|
|
d.state.OS.ExecPath,
|
|
"forkexec",
|
|
cname,
|
|
d.state.OS.LxcPath,
|
|
filepath.Join(d.RunPath(), "lxc.conf"),
|
|
req.Cwd,
|
|
fmt.Sprintf("%d", req.User),
|
|
fmt.Sprintf("%d", req.Group),
|
|
}
|
|
|
|
if d.state.OS.CoreScheduling && !d.state.OS.ContainerCoreScheduling {
|
|
args = append(args, "1")
|
|
} else {
|
|
args = append(args, "0")
|
|
}
|
|
|
|
args = append(args, "--")
|
|
args = append(args, "env")
|
|
args = append(args, envSlice...)
|
|
|
|
args = append(args, "--")
|
|
args = append(args, "cmd")
|
|
args = append(args, req.Command...)
|
|
|
|
cmd := exec.Cmd{}
|
|
cmd.Path = d.state.OS.ExecPath
|
|
cmd.Args = args
|
|
|
|
cmd.Stdin = nil
|
|
cmd.Stdout = logFile
|
|
cmd.Stderr = logFile
|
|
|
|
// Mitigation for CVE-2019-5736
|
|
useRexec := false
|
|
if d.expandedConfig["raw.idmap"] != "" {
|
|
err := instance.AllowedUnprivilegedOnlyMap(d.expandedConfig["raw.idmap"])
|
|
if err != nil {
|
|
useRexec = true
|
|
}
|
|
}
|
|
|
|
if util.IsTrue(d.expandedConfig["security.privileged"]) {
|
|
useRexec = true
|
|
}
|
|
|
|
if useRexec {
|
|
cmd.Env = append(os.Environ(), "LXC_MEMFD_REXEC=1")
|
|
}
|
|
|
|
// Setup communication PIPE
|
|
rStatus, wStatus, err := os.Pipe()
|
|
defer func() { _ = rStatus.Close() }()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cmd.ExtraFiles = []*os.File{stdin, stdout, stderr, wStatus}
|
|
err = cmd.Start()
|
|
_ = wStatus.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
attachedPid := linux.ReadPid(rStatus)
|
|
if attachedPid <= 0 {
|
|
_ = cmd.Wait()
|
|
d.logger.Error("Failed to retrieve PID of executing child process")
|
|
return nil, errors.New("Failed to retrieve PID of executing child process")
|
|
}
|
|
|
|
d.logger.Debug("Retrieved PID of executing child process", logger.Ctx{"attachedPid": attachedPid})
|
|
|
|
d.state.Events.SendLifecycle(d.project.Name, lifecycle.InstanceExec.Event(d, logger.Ctx{"command": req.Command}))
|
|
|
|
instCmd := &lxcCmd{
|
|
cmd: &cmd,
|
|
attachedChildPid: int(attachedPid),
|
|
}
|
|
|
|
return instCmd, nil
|
|
}
|
|
|
|
func (d *lxc) cpuStateUsage(cg *cgroup.CGroup) (int64, bool) {
|
|
if !d.state.OS.CGInfo.Supports(cgroup.CPUAcct, cg) {
|
|
return -1, false
|
|
}
|
|
|
|
value, err := cg.GetCPUAcctUsage()
|
|
if err != nil {
|
|
return -1, true
|
|
}
|
|
|
|
return value, true
|
|
}
|
|
|
|
func (d *lxc) cpuState() api.InstanceStateCPU {
|
|
cpu := api.InstanceStateCPU{}
|
|
|
|
cc, err := d.initLXC(false)
|
|
if err != nil {
|
|
return cpu
|
|
}
|
|
|
|
cg, err := d.cgroup(cc, true)
|
|
if err != nil {
|
|
return cpu
|
|
}
|
|
|
|
cpuUsage, ok := d.cpuStateUsage(cg)
|
|
if ok {
|
|
cpu.Usage = cpuUsage
|
|
}
|
|
|
|
cpuCount, err := cg.GetEffectiveCPUs()
|
|
if err != nil {
|
|
return cpu
|
|
}
|
|
|
|
limitPeriod, limitQuota, err := cg.GetCPUCfsLimit()
|
|
if err != nil {
|
|
return cpu
|
|
}
|
|
|
|
if limitQuota == -1 {
|
|
cpu.AllocatedTime = int64(cpuCount) * 1_000_000_000
|
|
} else {
|
|
cpu.AllocatedTime = 1_000_000_000 * limitQuota / limitPeriod
|
|
}
|
|
|
|
return cpu
|
|
}
|
|
|
|
func (d *lxc) diskState() map[string]api.InstanceStateDisk {
|
|
disk := map[string]api.InstanceStateDisk{}
|
|
|
|
for _, dev := range d.expandedDevices.Sorted() {
|
|
if dev.Config["type"] != "disk" {
|
|
continue
|
|
}
|
|
|
|
var usage *storagePools.VolumeUsage
|
|
|
|
if dev.Config["path"] == "/" {
|
|
pool, err := d.getStoragePool()
|
|
if err != nil {
|
|
d.logger.Error("Error loading storage pool", logger.Ctx{"err": err})
|
|
continue
|
|
}
|
|
|
|
usage, err = pool.GetInstanceUsage(d)
|
|
if err != nil {
|
|
if !errors.Is(err, storageDrivers.ErrNotSupported) {
|
|
d.logger.Error("Error getting disk usage", logger.Ctx{"err": err})
|
|
}
|
|
|
|
continue
|
|
}
|
|
} else if dev.Config["pool"] != "" {
|
|
pool, err := storagePools.LoadByName(d.state, dev.Config["pool"])
|
|
if err != nil {
|
|
d.logger.Error("Error loading storage pool", logger.Ctx{"poolName": dev.Config["pool"], "err": err})
|
|
continue
|
|
}
|
|
|
|
volName, _ := internalInstance.SplitVolumeSource(dev.Config["source"])
|
|
usage, err = pool.GetCustomVolumeUsage(d.Project().Name, volName)
|
|
if err != nil {
|
|
if !errors.Is(err, storageDrivers.ErrNotSupported) {
|
|
d.logger.Error("Error getting volume usage", logger.Ctx{"volume": dev.Config["source"], "err": err})
|
|
}
|
|
|
|
continue
|
|
}
|
|
} else {
|
|
continue
|
|
}
|
|
|
|
state := api.InstanceStateDisk{}
|
|
if usage != nil {
|
|
state.Usage = usage.Used
|
|
state.Total = usage.Total
|
|
}
|
|
|
|
disk[dev.Name] = state
|
|
}
|
|
|
|
return disk
|
|
}
|
|
|
|
func (d *lxc) memoryState() api.InstanceStateMemory {
|
|
memory := api.InstanceStateMemory{}
|
|
|
|
cc, err := d.initLXC(false)
|
|
if err != nil {
|
|
return memory
|
|
}
|
|
|
|
cg, err := d.cgroup(cc, true)
|
|
if err != nil {
|
|
return memory
|
|
}
|
|
|
|
if !d.state.OS.CGInfo.Supports(cgroup.Memory, cg) {
|
|
return memory
|
|
}
|
|
|
|
// Memory in bytes
|
|
value, err := cg.GetMemoryUsage()
|
|
if err == nil {
|
|
memory.Usage = value
|
|
}
|
|
|
|
// Memory peak in bytes
|
|
if d.state.OS.CGInfo.Supports(cgroup.MemoryMaxUsage, cg) {
|
|
value, err = cg.GetMemoryMaxUsage()
|
|
if err == nil {
|
|
memory.UsagePeak = value
|
|
}
|
|
}
|
|
|
|
// Memory total in bytes
|
|
value, err = cg.GetEffectiveMemoryLimit()
|
|
if err == nil {
|
|
memory.Total = value
|
|
}
|
|
|
|
if d.state.OS.CGInfo.Supports(cgroup.MemorySwapUsage, cg) {
|
|
// Swap in bytes
|
|
if memory.Usage > 0 {
|
|
value, err := cg.GetMemorySwapUsage()
|
|
if err == nil {
|
|
memory.SwapUsage = value
|
|
}
|
|
}
|
|
|
|
// Swap peak in bytes
|
|
if memory.UsagePeak > 0 {
|
|
value, err = cg.GetMemorySwapMaxUsage()
|
|
if err == nil {
|
|
memory.SwapUsagePeak = value
|
|
}
|
|
}
|
|
}
|
|
|
|
return memory
|
|
}
|
|
|
|
func (d *lxc) networkState(hostInterfaces []net.Interface) map[string]api.InstanceStateNetwork {
|
|
result := map[string]api.InstanceStateNetwork{}
|
|
|
|
pid := d.InitPID()
|
|
if pid < 1 {
|
|
return result
|
|
}
|
|
|
|
couldUseNetnsGetifaddrs := d.state.OS.NetnsGetifaddrs
|
|
if couldUseNetnsGetifaddrs {
|
|
nw, err := netutils.NetnsGetifaddrs(int32(pid), hostInterfaces)
|
|
if err != nil {
|
|
couldUseNetnsGetifaddrs = false
|
|
d.logger.Warn("Failed to retrieve network information via netlink", logger.Ctx{"pid": pid})
|
|
} else {
|
|
result = nw
|
|
}
|
|
}
|
|
|
|
if !couldUseNetnsGetifaddrs {
|
|
pidFd := d.inheritInitPidFd()
|
|
pidFdNr := "-1"
|
|
if pidFd != nil {
|
|
defer func() { _ = pidFd.Close() }()
|
|
pidFdNr = "3"
|
|
}
|
|
|
|
// Get the network state from the container
|
|
out, _, err := subprocess.RunCommandSplit(
|
|
context.TODO(),
|
|
nil,
|
|
[]*os.File{pidFd},
|
|
d.state.OS.ExecPath,
|
|
"forknet",
|
|
"info",
|
|
"--",
|
|
fmt.Sprintf("%d", pid),
|
|
pidFdNr)
|
|
// Process forkgetnet response
|
|
if err != nil {
|
|
d.logger.Error("Error calling 'forknet", logger.Ctx{"err": err, "pid": pid})
|
|
return result
|
|
}
|
|
|
|
// If we can use netns_getifaddrs() but it failed and the setns() +
|
|
// netns_getifaddrs() succeeded we should just always fallback to the
|
|
// setns() + netns_getifaddrs() style retrieval.
|
|
d.state.OS.NetnsGetifaddrs = false
|
|
|
|
nw := map[string]api.InstanceStateNetwork{}
|
|
err = json.Unmarshal([]byte(out), &nw)
|
|
if err != nil {
|
|
d.logger.Error("Failure to read forknet json", logger.Ctx{"err": err})
|
|
return result
|
|
}
|
|
|
|
result = nw
|
|
}
|
|
|
|
// Get host_name from volatile data if not set already.
|
|
for name, dev := range result {
|
|
if dev.HostName == "" {
|
|
dev.HostName = d.localConfig[fmt.Sprintf("volatile.%s.host_name", name)]
|
|
result[name] = dev
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (d *lxc) processesState(pid int) (int64, error) {
|
|
// Return 0 if not running
|
|
if pid == -1 {
|
|
return 0, errors.New("PID of LXC instance could not be initialized")
|
|
}
|
|
|
|
cc, err := d.initLXC(false)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
cg, err := d.cgroup(cc, true)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if d.state.OS.CGInfo.Supports(cgroup.Pids, cg) {
|
|
value, err := cg.GetProcessesUsage()
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return value, nil
|
|
}
|
|
|
|
pids := []int64{int64(pid)}
|
|
|
|
// Go through the pid list, adding new pids at the end so we go through them all
|
|
for i := range pids {
|
|
fname := fmt.Sprintf("/proc/%d/task/%d/children", pids[i], pids[i])
|
|
fcont, err := os.ReadFile(fname)
|
|
if err != nil {
|
|
// the process terminated during execution of this loop
|
|
continue
|
|
}
|
|
|
|
content := strings.Split(string(fcont), " ")
|
|
for j := range content {
|
|
pid, err := strconv.ParseInt(content[j], 10, 64)
|
|
if err == nil {
|
|
pids = append(pids, pid)
|
|
}
|
|
}
|
|
}
|
|
|
|
return int64(len(pids)), nil
|
|
}
|
|
|
|
// getStorageType returns the storage type of the instance's storage pool.
|
|
func (d *lxc) getStorageType() (string, error) {
|
|
pool, err := d.getStoragePool()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return pool.Driver().Info().Name, nil
|
|
}
|
|
|
|
// mount the instance's rootfs volume if needed.
|
|
func (d *lxc) mount() (*storagePools.MountInfo, error) {
|
|
pool, err := d.getStoragePool()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if d.IsSnapshot() {
|
|
mountInfo, err := pool.MountInstanceSnapshot(d, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return mountInfo, nil
|
|
}
|
|
|
|
mountInfo, err := pool.MountInstance(d, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return mountInfo, nil
|
|
}
|
|
|
|
// unmount the instance's rootfs volume if needed.
|
|
func (d *lxc) unmount() error {
|
|
pool, err := d.getStoragePool()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if d.IsSnapshot() {
|
|
err = pool.UnmountInstanceSnapshot(d, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
err = pool.UnmountInstance(d, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// insertMountGo inserts a mount into a container.
|
|
// This function is used for the seccomp notifier and so cannot call any
|
|
// functions that would cause LXC to talk to the container's monitor. Otherwise
|
|
// we'll have a deadlock (with a timeout but still). The InitPID() call here is
|
|
// the exception since the seccomp notifier will make sure to always pass a
|
|
// valid PID.
|
|
func (d *lxc) insertMountGo(source, target, fstype string, flags int, mntnsPID int, idmapType idmap.StorageType) error {
|
|
pid := mntnsPID
|
|
if pid <= 0 {
|
|
// Get the init PID
|
|
pid = d.InitPID()
|
|
if pid == -1 {
|
|
// Container isn't running
|
|
return errors.New("Can't insert mount into stopped container")
|
|
}
|
|
}
|
|
|
|
// Create the temporary mount target
|
|
var tmpMount string
|
|
var err error
|
|
if internalUtil.IsDir(source) {
|
|
tmpMount, err = os.MkdirTemp(d.ShmountsPath(), "incus_mount_")
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to create shmounts path: %s", err)
|
|
}
|
|
} else {
|
|
f, err := os.CreateTemp(d.ShmountsPath(), "incus_mount_")
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to create shmounts path: %s", err)
|
|
}
|
|
|
|
tmpMount = f.Name()
|
|
_ = f.Close()
|
|
}
|
|
|
|
defer func() { _ = os.Remove(tmpMount) }()
|
|
|
|
// Mount the filesystem
|
|
err = unix.Mount(source, tmpMount, fstype, uintptr(flags), "")
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to setup temporary mount: %s", err)
|
|
}
|
|
|
|
defer func() { _ = unix.Unmount(tmpMount, unix.MNT_DETACH) }()
|
|
|
|
// Ensure that only flags modifying mount _properties_ make it through.
|
|
// Strip things such as MS_BIND which would cause the creation of a
|
|
// shifted mount to be skipped.
|
|
// (Fyi, this is just one of the reasons why multiplexers are bad;
|
|
// specifically when they do heinous things such as confusing flags
|
|
// with commands.)
|
|
|
|
// This is why multiplexers are bad
|
|
shiftfsFlags := (flags & (unix.MS_RDONLY |
|
|
unix.MS_NOSUID |
|
|
unix.MS_NODEV |
|
|
unix.MS_NOEXEC |
|
|
unix.MS_DIRSYNC |
|
|
unix.MS_NOATIME |
|
|
unix.MS_NODIRATIME))
|
|
|
|
// Move the mount inside the container
|
|
mntsrc := filepath.Join("/dev/.incus-mounts", filepath.Base(tmpMount))
|
|
pidStr := fmt.Sprintf("%d", pid)
|
|
|
|
pidFdNr, pidFd := seccomp.MakePidFd(pid, d.state)
|
|
if pidFdNr >= 0 {
|
|
defer func() { _ = pidFd.Close() }()
|
|
}
|
|
|
|
if !strings.HasPrefix(target, "/") {
|
|
target = "/" + target
|
|
}
|
|
|
|
_, err = subprocess.RunCommandInheritFds(
|
|
context.Background(),
|
|
[]*os.File{pidFd},
|
|
d.state.OS.ExecPath,
|
|
"forkmount",
|
|
"go-mount",
|
|
"--",
|
|
pidStr,
|
|
fmt.Sprintf("%d", pidFdNr),
|
|
mntsrc,
|
|
target,
|
|
string(idmapType),
|
|
fmt.Sprintf("%d", shiftfsFlags))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *lxc) insertMountLXC(source, target, fstype string, flags int) error {
|
|
cname := project.Instance(d.Project().Name, d.Name())
|
|
configPath := filepath.Join(d.RunPath(), "lxc.conf")
|
|
if fstype == "" {
|
|
fstype = "none"
|
|
}
|
|
|
|
if !strings.HasPrefix(target, "/") {
|
|
target = "/" + target
|
|
}
|
|
|
|
_, err := subprocess.RunCommand(
|
|
d.state.OS.ExecPath,
|
|
"forkmount",
|
|
"lxc-mount",
|
|
"--",
|
|
cname,
|
|
d.state.OS.LxcPath,
|
|
configPath,
|
|
source,
|
|
target,
|
|
fstype,
|
|
fmt.Sprintf("%d", flags))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *lxc) moveMount(source, target, fstype string, flags int, idmapType idmap.StorageType) error {
|
|
// Get the init PID
|
|
pid := d.InitPID()
|
|
if pid == -1 {
|
|
// Container isn't running
|
|
return errors.New("Can't insert mount into stopped container")
|
|
}
|
|
|
|
switch idmapType {
|
|
case idmap.StorageTypeIdmapped:
|
|
case idmap.StorageTypeNone:
|
|
default:
|
|
return errors.New("Invalid idmap value specified")
|
|
}
|
|
|
|
pidFdNr, pidFd := seccomp.MakePidFd(pid, d.state)
|
|
if pidFdNr >= 0 {
|
|
defer func() { _ = pidFd.Close() }()
|
|
}
|
|
|
|
pidStr := fmt.Sprintf("%d", pid)
|
|
|
|
if !strings.HasPrefix(target, "/") {
|
|
target = "/" + target
|
|
}
|
|
|
|
_, err := subprocess.RunCommandInheritFds(
|
|
context.Background(),
|
|
[]*os.File{pidFd},
|
|
d.state.OS.ExecPath,
|
|
"forkmount",
|
|
"move-mount",
|
|
"--",
|
|
pidStr,
|
|
fmt.Sprintf("%d", pidFdNr),
|
|
fstype,
|
|
source,
|
|
target,
|
|
string(idmapType),
|
|
fmt.Sprintf("%d", flags))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *lxc) insertMount(source, target, fstype string, flags int, idmapType idmap.StorageType) error {
|
|
if d.state.OS.IdmappedMounts && idmapType == idmap.StorageTypeIdmapped {
|
|
return d.moveMount(source, target, fstype, flags, idmapType)
|
|
}
|
|
|
|
if d.state.OS.LXCFeatures["mount_injection_file"] && idmapType == idmap.StorageTypeNone {
|
|
return d.insertMountLXC(source, target, fstype, flags)
|
|
}
|
|
|
|
return d.insertMountGo(source, target, fstype, flags, -1, idmapType)
|
|
}
|
|
|
|
func (d *lxc) removeMount(mount string) error {
|
|
// Get the init PID
|
|
pid := d.InitPID()
|
|
if pid == -1 {
|
|
// Container isn't running
|
|
return errors.New("Can't remove mount from stopped container")
|
|
}
|
|
|
|
if d.state.OS.LXCFeatures["mount_injection_file"] {
|
|
configPath := filepath.Join(d.RunPath(), "lxc.conf")
|
|
cname := project.Instance(d.Project().Name, d.Name())
|
|
|
|
if !strings.HasPrefix(mount, "/") {
|
|
mount = "/" + mount
|
|
}
|
|
|
|
_, err := subprocess.RunCommand(
|
|
d.state.OS.ExecPath,
|
|
"forkmount",
|
|
"lxc-umount",
|
|
"--",
|
|
cname,
|
|
d.state.OS.LxcPath,
|
|
configPath,
|
|
mount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// Remove the mount from the container
|
|
pidFd := d.inheritInitPidFd()
|
|
pidFdNr := "-1"
|
|
if pidFd != nil {
|
|
defer func() { _ = pidFd.Close() }()
|
|
pidFdNr = "3"
|
|
}
|
|
|
|
_, err := subprocess.RunCommandInheritFds(
|
|
context.TODO(),
|
|
[]*os.File{pidFd},
|
|
d.state.OS.ExecPath,
|
|
"forkmount",
|
|
"go-umount",
|
|
"--",
|
|
fmt.Sprintf("%d", pid),
|
|
pidFdNr,
|
|
mount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// InsertSeccompUnixDevice inserts a seccomp device.
|
|
func (d *lxc) InsertSeccompUnixDevice(prefix string, m deviceConfig.Device, pid int) error {
|
|
if pid < 0 {
|
|
return errors.New("Invalid request PID specified")
|
|
}
|
|
|
|
rootLink := fmt.Sprintf("/proc/%d/root", pid)
|
|
rootPath, err := os.Readlink(rootLink)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
uid, gid, _, _, err := seccomp.TaskIDs(pid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
idmapset, err := d.CurrentIdmap()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
nsuid, nsgid := idmapset.ShiftFromNS(uid, gid)
|
|
m["uid"] = fmt.Sprintf("%d", nsuid)
|
|
m["gid"] = fmt.Sprintf("%d", nsgid)
|
|
|
|
if !path.IsAbs(m["path"]) {
|
|
cwdLink := fmt.Sprintf("/proc/%d/cwd", pid)
|
|
prefixPath, err := os.Readlink(cwdLink)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
prefixPath = strings.TrimPrefix(prefixPath, rootPath)
|
|
m["path"] = filepath.Join(rootPath, prefixPath, m["path"])
|
|
} else {
|
|
m["path"] = filepath.Join(rootPath, m["path"])
|
|
}
|
|
|
|
idmapSet, err := d.CurrentIdmap()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dev, err := device.UnixDeviceCreate(d.state, idmapSet, d.DevicesPath(), prefix, m, true)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to setup device: %s", err)
|
|
}
|
|
|
|
devPath := dev.HostPath
|
|
tgtPath := dev.RelativePath
|
|
|
|
// Bind-mount it into the container
|
|
defer func() { _ = os.Remove(devPath) }()
|
|
return d.insertMountGo(devPath, tgtPath, "none", unix.MS_BIND, pid, idmap.StorageTypeNone)
|
|
}
|
|
|
|
func (d *lxc) removeUnixDevices() error {
|
|
// Check that we indeed have devices to remove
|
|
if !util.PathExists(d.DevicesPath()) {
|
|
return nil
|
|
}
|
|
|
|
// Load the directory listing
|
|
dents, err := os.ReadDir(d.DevicesPath())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Go through all the unix devices
|
|
for _, f := range dents {
|
|
// Skip non-Unix devices
|
|
if !strings.HasPrefix(f.Name(), "forkmknod.unix.") && !strings.HasPrefix(f.Name(), "unix.") && !strings.HasPrefix(f.Name(), "infiniband.unix.") {
|
|
continue
|
|
}
|
|
|
|
// Remove the entry
|
|
devicePath := filepath.Join(d.DevicesPath(), f.Name())
|
|
err := os.Remove(devicePath)
|
|
if err != nil {
|
|
d.logger.Error("Failed removing unix device", logger.Ctx{"err": err, "path": devicePath})
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// FillNetworkDevice takes a nic or infiniband device type and enriches it with automatically
|
|
// generated name and hwaddr properties if these are missing from the device.
|
|
func (d *lxc) FillNetworkDevice(name string, m deviceConfig.Device) (deviceConfig.Device, error) {
|
|
var err error
|
|
newDevice := m.Clone()
|
|
|
|
// Function to try and guess an available name
|
|
nextInterfaceName := func() (string, error) {
|
|
devNames := []string{}
|
|
|
|
// Include all static interface names
|
|
for _, dev := range d.expandedDevices.Sorted() {
|
|
if dev.Config["name"] != "" && !slices.Contains(devNames, dev.Config["name"]) {
|
|
devNames = append(devNames, dev.Config["name"])
|
|
}
|
|
}
|
|
|
|
// Include all currently allocated interface names
|
|
for k, v := range d.expandedConfig {
|
|
if !strings.HasPrefix(k, internalInstance.ConfigVolatilePrefix) {
|
|
continue
|
|
}
|
|
|
|
fields := strings.SplitN(k, ".", 3)
|
|
if len(fields) != 3 {
|
|
continue
|
|
}
|
|
|
|
if fields[2] != "name" || slices.Contains(devNames, v) {
|
|
continue
|
|
}
|
|
|
|
devNames = append(devNames, v)
|
|
}
|
|
|
|
// Attempt to include all existing interfaces
|
|
cname := project.Instance(d.Project().Name, d.Name())
|
|
cc, err := liblxc.NewContainer(cname, d.state.OS.LxcPath)
|
|
if err == nil {
|
|
defer func() { _ = cc.Release() }()
|
|
|
|
interfaces, err := cc.Interfaces()
|
|
if err == nil {
|
|
for _, name := range interfaces {
|
|
if slices.Contains(devNames, name) {
|
|
continue
|
|
}
|
|
|
|
devNames = append(devNames, name)
|
|
}
|
|
}
|
|
}
|
|
|
|
i := 0
|
|
name := ""
|
|
for {
|
|
if m["type"] == "infiniband" {
|
|
name = fmt.Sprintf("ib%d", i)
|
|
} else {
|
|
name = fmt.Sprintf("eth%d", i)
|
|
}
|
|
|
|
// Find a free device name
|
|
if !slices.Contains(devNames, name) {
|
|
return name, nil
|
|
}
|
|
|
|
i++
|
|
}
|
|
}
|
|
|
|
nicType, err := nictype.NICType(d.state, d.Project().Name, m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
isPhysicalWithBridge := device.IsPhysicalNICWithBridge(d.state, d.Project().Name, m)
|
|
|
|
// Fill in the MAC address.
|
|
if (!slices.Contains([]string{"physical", "ipvlan"}, nicType) || isPhysicalWithBridge) && m["hwaddr"] == "" {
|
|
configKey := fmt.Sprintf("volatile.%s.hwaddr", name)
|
|
volatileHwaddr := d.localConfig[configKey]
|
|
if volatileHwaddr == "" {
|
|
// Generate a new MAC address.
|
|
volatileHwaddr, err = instance.DeviceNextInterfaceHWAddr(d.MACPattern())
|
|
if err != nil || volatileHwaddr == "" {
|
|
return nil, fmt.Errorf("Failed generating %q: %w", configKey, err)
|
|
}
|
|
|
|
// Update the database and update volatileHwaddr with stored value.
|
|
volatileHwaddr, err = d.insertConfigkey(configKey, volatileHwaddr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed storing generated config key %q: %w", configKey, err)
|
|
}
|
|
|
|
// Set stored value into current instance config.
|
|
d.localConfig[configKey] = volatileHwaddr
|
|
d.expandedConfig[configKey] = volatileHwaddr
|
|
}
|
|
|
|
if volatileHwaddr == "" {
|
|
return nil, fmt.Errorf("Failed getting %q", configKey)
|
|
}
|
|
|
|
newDevice["hwaddr"] = volatileHwaddr
|
|
}
|
|
|
|
// Fill in the interface name.
|
|
if m["name"] == "" {
|
|
configKey := fmt.Sprintf("volatile.%s.name", name)
|
|
volatileName := d.localConfig[configKey]
|
|
if volatileName == "" {
|
|
// Generate a new interface name.
|
|
volatileName, err = nextInterfaceName()
|
|
if err != nil || volatileName == "" {
|
|
return nil, fmt.Errorf("Failed generating %q: %w", configKey, err)
|
|
}
|
|
|
|
// Update the database and update volatileName with stored value.
|
|
volatileName, err = d.insertConfigkey(configKey, volatileName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed storing generated config key %q: %w", configKey, err)
|
|
}
|
|
|
|
// Set stored value into current instance config.
|
|
d.localConfig[configKey] = volatileName
|
|
d.expandedConfig[configKey] = volatileName
|
|
}
|
|
|
|
if volatileName == "" {
|
|
return nil, fmt.Errorf("Failed getting %q", configKey)
|
|
}
|
|
|
|
newDevice["name"] = volatileName
|
|
}
|
|
|
|
return newDevice, nil
|
|
}
|
|
|
|
func (d *lxc) removeDiskDevices() error {
|
|
// Check that we indeed have devices to remove
|
|
if !util.PathExists(d.DevicesPath()) {
|
|
return nil
|
|
}
|
|
|
|
// Load the directory listing
|
|
dents, err := os.ReadDir(d.DevicesPath())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Go through all the unix devices
|
|
for _, f := range dents {
|
|
// Skip non-disk devices
|
|
if !strings.HasPrefix(f.Name(), "disk.") {
|
|
continue
|
|
}
|
|
|
|
// Always try to unmount the host side
|
|
_ = unix.Unmount(filepath.Join(d.DevicesPath(), f.Name()), unix.MNT_DETACH)
|
|
|
|
// Remove the entry
|
|
diskPath := filepath.Join(d.DevicesPath(), f.Name())
|
|
err := os.Remove(diskPath)
|
|
if err != nil {
|
|
d.logger.Error("Failed to remove disk device path", logger.Ctx{"err": err, "path": diskPath})
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsFrozen returns if instance is frozen.
|
|
func (d *lxc) IsFrozen() bool {
|
|
return d.statusCode() == api.Frozen
|
|
}
|
|
|
|
// IsNesting returns if instance is nested.
|
|
func (d *lxc) IsNesting() bool {
|
|
return util.IsTrue(d.expandedConfig["security.nesting"])
|
|
}
|
|
|
|
func (d *lxc) isCurrentlyPrivileged() bool {
|
|
if !d.IsRunning() {
|
|
return d.IsPrivileged()
|
|
}
|
|
|
|
idmap, err := d.CurrentIdmap()
|
|
if err != nil {
|
|
return d.IsPrivileged()
|
|
}
|
|
|
|
return idmap == nil
|
|
}
|
|
|
|
// IsPrivileged returns if instance is privileged.
|
|
func (d *lxc) IsPrivileged() bool {
|
|
return util.IsTrue(d.expandedConfig["security.privileged"])
|
|
}
|
|
|
|
// IsRunning returns if instance is running.
|
|
func (d *lxc) IsRunning() bool {
|
|
return d.isRunningStatusCode(d.statusCode())
|
|
}
|
|
|
|
// CanMigrate returns whether the instance can be migrated.
|
|
func (d *lxc) CanMigrate() string {
|
|
return d.canMigrate(d)
|
|
}
|
|
|
|
// LockExclusive attempts to get exclusive access to the instance's root volume.
|
|
func (d *lxc) LockExclusive() (*operationlock.InstanceOperation, error) {
|
|
if d.IsRunning() {
|
|
return nil, errors.New("Instance is running")
|
|
}
|
|
|
|
// Prevent concurrent operations the instance.
|
|
op, err := operationlock.Create(d.Project().Name, d.Name(), d.op, operationlock.ActionCreate, false, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Stop forkfile as otherwise it will hold the root volume open preventing unmount.
|
|
d.stopForkfile(false)
|
|
|
|
return op, err
|
|
}
|
|
|
|
// InitPID returns PID of init process.
|
|
func (d *lxc) InitPID() int {
|
|
// Load the go-lxc struct
|
|
cc, err := d.initLXC(false)
|
|
if err != nil {
|
|
return -1
|
|
}
|
|
|
|
return cc.InitPid()
|
|
}
|
|
|
|
// InitPidFd returns pidfd of init process.
|
|
func (d *lxc) InitPidFd() (*os.File, error) {
|
|
// Load the go-lxc struct
|
|
cc, err := d.initLXC(false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return cc.InitPidFd()
|
|
}
|
|
|
|
// DevptsFd returns dirfd of devpts mount.
|
|
func (d *lxc) DevptsFd() (*os.File, error) {
|
|
// Load the go-lxc struct
|
|
cc, err := d.initLXC(false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer d.release()
|
|
|
|
if !liblxc.HasAPIExtension("devpts_fd") {
|
|
return nil, errors.New("Missing devpts_fd extension")
|
|
}
|
|
|
|
return cc.DevptsFd()
|
|
}
|
|
|
|
// CurrentIdmap returns current IDMAP.
|
|
func (d *lxc) CurrentIdmap() (*idmap.Set, error) {
|
|
jsonIdmap, ok := d.LocalConfig()["volatile.idmap.current"]
|
|
if !ok {
|
|
return d.DiskIdmap()
|
|
}
|
|
|
|
return idmap.NewSetFromJSON(jsonIdmap)
|
|
}
|
|
|
|
// DiskIdmap returns DISK IDMAP.
|
|
func (d *lxc) DiskIdmap() (*idmap.Set, error) {
|
|
jsonIdmap, ok := d.LocalConfig()["volatile.last_state.idmap"]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
return idmap.NewSetFromJSON(jsonIdmap)
|
|
}
|
|
|
|
// NextIdmap returns next IDMAP.
|
|
func (d *lxc) NextIdmap() (*idmap.Set, error) {
|
|
jsonIdmap, ok := d.LocalConfig()["volatile.idmap.next"]
|
|
if !ok {
|
|
return d.CurrentIdmap()
|
|
}
|
|
|
|
return idmap.NewSetFromJSON(jsonIdmap)
|
|
}
|
|
|
|
// statusCode returns instance status code.
|
|
func (d *lxc) statusCode() api.StatusCode {
|
|
// Shortcut to avoid spamming liblxc during ongoing operations.
|
|
op := operationlock.Get(d.Project().Name, d.Name())
|
|
if op != nil {
|
|
if op.Action() == operationlock.ActionStart {
|
|
return api.Stopped
|
|
}
|
|
|
|
if op.Action() == operationlock.ActionStop {
|
|
if util.IsTrue(d.LocalConfig()["volatile.last_state.ready"]) {
|
|
return api.Ready
|
|
}
|
|
|
|
return api.Running
|
|
}
|
|
}
|
|
|
|
state, err := d.getLxcState()
|
|
if err != nil {
|
|
return api.Error
|
|
}
|
|
|
|
statusCode := lxcStatusCode(state)
|
|
|
|
if statusCode == api.Running && util.IsTrue(d.LocalConfig()["volatile.last_state.ready"]) {
|
|
return api.Ready
|
|
}
|
|
|
|
return statusCode
|
|
}
|
|
|
|
// State returns instance state.
|
|
func (d *lxc) State() string {
|
|
return strings.ToUpper(d.statusCode().String())
|
|
}
|
|
|
|
// LogFilePath log file path.
|
|
func (d *lxc) LogFilePath() string {
|
|
return filepath.Join(d.LogPath(), "lxc.log")
|
|
}
|
|
|
|
func (d *lxc) CGroup() (*cgroup.CGroup, error) {
|
|
// Load the go-lxc struct
|
|
cc, err := d.initLXC(false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return d.cgroup(cc, true)
|
|
}
|
|
|
|
func (d *lxc) cgroup(cc *liblxc.Container, running bool) (*cgroup.CGroup, error) {
|
|
if cc == nil {
|
|
return nil, errors.New("Container not initialized for cgroup")
|
|
}
|
|
|
|
rw := lxcCgroupReadWriter{}
|
|
rw.cc = cc
|
|
rw.running = running
|
|
|
|
cg, err := cgroup.New(&rw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cg.UnifiedCapable = liblxc.HasAPIExtension("cgroup2")
|
|
return cg, nil
|
|
}
|
|
|
|
type lxcCgroupReadWriter struct {
|
|
cc *liblxc.Container
|
|
running bool
|
|
}
|
|
|
|
func (rw *lxcCgroupReadWriter) Get(version cgroup.Backend, controller string, key string) (string, error) {
|
|
if !rw.running {
|
|
lxcKey := fmt.Sprintf("lxc.cgroup.%s", key)
|
|
|
|
if version == cgroup.V2 {
|
|
lxcKey = fmt.Sprintf("lxc.cgroup2.%s", key)
|
|
}
|
|
|
|
return strings.Join(rw.cc.ConfigItem(lxcKey), "\n"), nil
|
|
}
|
|
|
|
return strings.Join(rw.cc.CgroupItem(key), "\n"), nil
|
|
}
|
|
|
|
func (rw *lxcCgroupReadWriter) Set(version cgroup.Backend, controller string, key string, value string) error {
|
|
if !rw.running {
|
|
if version == cgroup.V1 {
|
|
return lxcSetConfigItem(rw.cc, fmt.Sprintf("lxc.cgroup.%s", key), value)
|
|
}
|
|
|
|
return lxcSetConfigItem(rw.cc, fmt.Sprintf("lxc.cgroup2.%s", key), value)
|
|
}
|
|
|
|
return rw.cc.SetCgroupItem(key, value)
|
|
}
|
|
|
|
// UpdateBackupFile writes the instance's backup.yaml file to storage.
|
|
func (d *lxc) UpdateBackupFile() error {
|
|
// Prevent concurrent updates to the backup file.
|
|
unlock, err := d.updateBackupFileLock(context.Background())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer unlock()
|
|
|
|
// Write the current instance state to backup file.
|
|
pool, err := d.getStoragePool()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return pool.UpdateInstanceBackupFile(d, true, nil)
|
|
}
|
|
|
|
// Info returns "lxc" and the currently loaded version of LXC.
|
|
func (d *lxc) Info() instance.Info {
|
|
return instance.Info{
|
|
Name: "lxc",
|
|
Version: liblxc.Version(),
|
|
Type: instancetype.Container,
|
|
Error: nil,
|
|
}
|
|
}
|
|
|
|
func (d *lxc) Metrics(hostInterfaces []net.Interface) (*metrics.MetricSet, error) {
|
|
out := metrics.NewMetricSet(map[string]string{"project": d.project.Name, "name": d.name, "type": instancetype.Container.String()})
|
|
|
|
if !d.IsRunning() {
|
|
return nil, ErrInstanceIsStopped
|
|
}
|
|
|
|
cc, err := d.initLXC(false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Load cgroup abstraction
|
|
cg, err := d.cgroup(cc, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get Memory limit.
|
|
memoryLimit, err := cg.GetEffectiveMemoryLimit()
|
|
if err != nil {
|
|
d.logger.Warn("Failed getting effective memory limit", logger.Ctx{"err": err})
|
|
}
|
|
|
|
memoryCached := int64(0)
|
|
|
|
// Get memory stats.
|
|
memStats, err := cg.GetMemoryStats()
|
|
if err != nil {
|
|
d.logger.Warn("Failed to get memory stats", logger.Ctx{"err": err})
|
|
} else {
|
|
for k, v := range memStats {
|
|
var metricType metrics.MetricType
|
|
|
|
switch k {
|
|
case "active_anon":
|
|
metricType = metrics.MemoryActiveAnonBytes
|
|
case "active_file":
|
|
metricType = metrics.MemoryActiveFileBytes
|
|
case "active":
|
|
metricType = metrics.MemoryActiveBytes
|
|
case "inactive_anon":
|
|
metricType = metrics.MemoryInactiveAnonBytes
|
|
case "inactive_file":
|
|
metricType = metrics.MemoryInactiveFileBytes
|
|
case "inactive":
|
|
metricType = metrics.MemoryInactiveBytes
|
|
case "unevictable":
|
|
metricType = metrics.MemoryUnevictableBytes
|
|
case "writeback":
|
|
metricType = metrics.MemoryWritebackBytes
|
|
case "dirty":
|
|
metricType = metrics.MemoryDirtyBytes
|
|
case "mapped":
|
|
metricType = metrics.MemoryMappedBytes
|
|
case "rss":
|
|
metricType = metrics.MemoryRSSBytes
|
|
case "shmem":
|
|
metricType = metrics.MemoryShmemBytes
|
|
case "cache":
|
|
metricType = metrics.MemoryCachedBytes
|
|
memoryCached = int64(v)
|
|
}
|
|
|
|
out.AddSamples(metricType, metrics.Sample{Value: float64(v)})
|
|
}
|
|
}
|
|
|
|
// Get memory usage.
|
|
memoryUsage, err := cg.GetMemoryUsage()
|
|
if err != nil {
|
|
d.logger.Warn("Failed to get memory usage", logger.Ctx{"err": err})
|
|
}
|
|
|
|
if memoryLimit > 0 {
|
|
out.AddSamples(metrics.MemoryMemTotalBytes, metrics.Sample{Value: float64(memoryLimit)})
|
|
out.AddSamples(metrics.MemoryMemAvailableBytes, metrics.Sample{Value: float64(memoryLimit - memoryUsage + memoryCached)})
|
|
out.AddSamples(metrics.MemoryMemFreeBytes, metrics.Sample{Value: float64(memoryLimit - memoryUsage)})
|
|
}
|
|
|
|
// Get oom kills.
|
|
oomKills, err := cg.GetOOMKills()
|
|
if err != nil {
|
|
d.logger.Warn("Failed to get oom kills", logger.Ctx{"err": err})
|
|
}
|
|
|
|
out.AddSamples(metrics.MemoryOOMKillsTotal, metrics.Sample{Value: float64(oomKills)})
|
|
|
|
// Handle swap.
|
|
if d.state.OS.CGInfo.Supports(cgroup.MemorySwapUsage, cg) {
|
|
swapUsage, err := cg.GetMemorySwapUsage()
|
|
if err != nil {
|
|
d.logger.Warn("Failed to get swap usage", logger.Ctx{"err": err})
|
|
} else {
|
|
out.AddSamples(metrics.MemorySwapBytes, metrics.Sample{Value: float64(swapUsage)})
|
|
}
|
|
}
|
|
|
|
// Get CPU stats
|
|
usage, err := cg.GetCPUAcctUsageAll()
|
|
if err != nil {
|
|
d.logger.Warn("Failed to get CPU usage", logger.Ctx{"err": err})
|
|
} else {
|
|
for cpu, stats := range usage {
|
|
cpuID := strconv.Itoa(int(cpu))
|
|
|
|
out.AddSamples(metrics.CPUSecondsTotal, metrics.Sample{Value: float64(stats.System) / 1000000000, Labels: map[string]string{"mode": "system", "cpu": cpuID}})
|
|
out.AddSamples(metrics.CPUSecondsTotal, metrics.Sample{Value: float64(stats.User) / 1000000000, Labels: map[string]string{"mode": "user", "cpu": cpuID}})
|
|
}
|
|
}
|
|
|
|
// Get CPUs.
|
|
CPUs, err := cg.GetEffectiveCPUs()
|
|
if err != nil {
|
|
d.logger.Warn("Failed to get CPUs", logger.Ctx{"err": err})
|
|
} else {
|
|
out.AddSamples(metrics.CPUs, metrics.Sample{Value: float64(CPUs)})
|
|
}
|
|
|
|
// Get disk stats
|
|
diskStats, err := cg.GetIOStats()
|
|
if err != nil {
|
|
d.logger.Warn("Failed to get disk stats", logger.Ctx{"err": err})
|
|
} else {
|
|
for disk, stats := range diskStats {
|
|
labels := map[string]string{"device": disk}
|
|
|
|
out.AddSamples(metrics.DiskReadBytesTotal, metrics.Sample{Value: float64(stats.ReadBytes), Labels: labels})
|
|
out.AddSamples(metrics.DiskReadsCompletedTotal, metrics.Sample{Value: float64(stats.ReadsCompleted), Labels: labels})
|
|
out.AddSamples(metrics.DiskWrittenBytesTotal, metrics.Sample{Value: float64(stats.WrittenBytes), Labels: labels})
|
|
out.AddSamples(metrics.DiskWritesCompletedTotal, metrics.Sample{Value: float64(stats.WritesCompleted), Labels: labels})
|
|
}
|
|
}
|
|
|
|
// Get filesystem stats
|
|
fsStats, err := d.getFSStats()
|
|
if err != nil {
|
|
d.logger.Warn("Failed to get fs stats", logger.Ctx{"err": err})
|
|
} else {
|
|
out.Merge(fsStats)
|
|
}
|
|
|
|
// Get network stats
|
|
networkState := d.networkState(hostInterfaces)
|
|
|
|
for name, state := range networkState {
|
|
labels := map[string]string{"device": name}
|
|
|
|
out.AddSamples(metrics.NetworkReceiveBytesTotal, metrics.Sample{Value: float64(state.Counters.BytesReceived), Labels: labels})
|
|
out.AddSamples(metrics.NetworkReceivePacketsTotal, metrics.Sample{Value: float64(state.Counters.PacketsReceived), Labels: labels})
|
|
out.AddSamples(metrics.NetworkTransmitBytesTotal, metrics.Sample{Value: float64(state.Counters.BytesSent), Labels: labels})
|
|
out.AddSamples(metrics.NetworkTransmitPacketsTotal, metrics.Sample{Value: float64(state.Counters.PacketsSent), Labels: labels})
|
|
out.AddSamples(metrics.NetworkReceiveErrsTotal, metrics.Sample{Value: float64(state.Counters.ErrorsReceived), Labels: labels})
|
|
out.AddSamples(metrics.NetworkTransmitErrsTotal, metrics.Sample{Value: float64(state.Counters.ErrorsSent), Labels: labels})
|
|
out.AddSamples(metrics.NetworkReceiveDropTotal, metrics.Sample{Value: float64(state.Counters.PacketsDroppedInbound), Labels: labels})
|
|
out.AddSamples(metrics.NetworkTransmitDropTotal, metrics.Sample{Value: float64(state.Counters.PacketsDroppedOutbound), Labels: labels})
|
|
}
|
|
|
|
// Get number of processes
|
|
pids, err := d.processesState(d.InitPID())
|
|
if err != nil {
|
|
d.logger.Warn("Failed to get total number of processes", logger.Ctx{"err": err})
|
|
} else {
|
|
out.AddSamples(metrics.ProcsTotal, metrics.Sample{Value: float64(pids)})
|
|
}
|
|
|
|
// Set the timestamps
|
|
startedAt, err := d.processStartedAt(d.InitPID())
|
|
if err != nil {
|
|
d.logger.Warn("Failed to get instance startup time", logger.Ctx{"err": err})
|
|
} else {
|
|
out.AddSamples(metrics.BootTimeSeconds, metrics.Sample{Value: float64(startedAt.Unix())})
|
|
}
|
|
|
|
out.AddSamples(metrics.TimeSeconds, metrics.Sample{Value: float64(time.Now().Unix())})
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func (d *lxc) getFSStats() (*metrics.MetricSet, error) {
|
|
type mountInfo struct {
|
|
Mountpoint string
|
|
FSType string
|
|
}
|
|
|
|
out := metrics.NewMetricSet(nil)
|
|
|
|
mounts, err := os.ReadFile("/proc/mounts")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to read /proc/mounts: %w", err)
|
|
}
|
|
|
|
mountMap := make(map[string]mountInfo)
|
|
scanner := bufio.NewScanner(bytes.NewReader(mounts))
|
|
|
|
for scanner.Scan() {
|
|
fields := strings.Fields(scanner.Text())
|
|
|
|
mountMap[fields[0]] = mountInfo{Mountpoint: fields[1], FSType: fields[2]}
|
|
}
|
|
|
|
// Get disk devices
|
|
for _, dev := range d.expandedDevices {
|
|
if dev["type"] != "disk" || dev["path"] == "" {
|
|
continue
|
|
}
|
|
|
|
var statfs *unix.Statfs_t
|
|
labels := make(map[string]string)
|
|
realDev := ""
|
|
|
|
if dev["pool"] != "" {
|
|
// Expected volume name.
|
|
var volName string
|
|
var volType storageDrivers.VolumeType
|
|
if dev["source"] != "" {
|
|
volName = project.StorageVolume(d.project.Name, dev["source"])
|
|
volType = storageDrivers.VolumeTypeCustom
|
|
} else {
|
|
volName = project.Instance(d.project.Name, d.name)
|
|
volType = storageDrivers.VolumeTypeContainer
|
|
}
|
|
|
|
// Check that we have a mountpoint.
|
|
mountpoint := storageDrivers.GetVolumeMountPath(dev["pool"], volType, volName)
|
|
if mountpoint == "" || !util.PathExists(mountpoint) {
|
|
continue
|
|
}
|
|
|
|
// Grab the filesystem information.
|
|
statfs, err = linux.StatVFS(mountpoint)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to stat %s: %w", mountpoint, err)
|
|
}
|
|
|
|
// Grab the pool information to compare.
|
|
poolStatfs, err := linux.StatVFS(internalUtil.VarPath("storage-pools", dev["pool"]))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to stat %s: %w", mountpoint, err)
|
|
}
|
|
|
|
// Check if we have actual mount-specific information.
|
|
if statfs.Type == poolStatfs.Type && statfs.Blocks == poolStatfs.Blocks && statfs.Bfree == poolStatfs.Bfree && statfs.Bavail == poolStatfs.Bavail {
|
|
continue
|
|
}
|
|
|
|
// Check if mountPath is in mountMap
|
|
isMounted := false
|
|
|
|
for mountDev, mountInfo := range mountMap {
|
|
if mountInfo.Mountpoint != mountpoint {
|
|
continue
|
|
}
|
|
|
|
isMounted = true
|
|
realDev = mountDev
|
|
break
|
|
}
|
|
|
|
if !isMounted {
|
|
realDev = dev["source"]
|
|
}
|
|
} else {
|
|
// Skip special disks.
|
|
if device.IsSpecialDisk(dev["source"]) {
|
|
continue
|
|
}
|
|
|
|
source := dev["source"]
|
|
|
|
statfs, err = linux.StatVFS(source)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to stat %s: %w", dev["source"], err)
|
|
}
|
|
|
|
isMounted := false
|
|
|
|
// Check if mountPath is in mountMap
|
|
for mountDev, mountInfo := range mountMap {
|
|
if mountInfo.Mountpoint != source {
|
|
continue
|
|
}
|
|
|
|
isMounted = true
|
|
stat := unix.Stat_t{}
|
|
|
|
// Check if dev has a backing file
|
|
err = unix.Stat(source, &stat)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to stat %s: %w", dev["source"], err)
|
|
}
|
|
|
|
backingFilePath := fmt.Sprintf("/sys/dev/block/%d:%d/loop/backing_file", unix.Major(uint64(stat.Dev)), unix.Minor(uint64(stat.Dev)))
|
|
|
|
if util.PathExists(backingFilePath) {
|
|
// Read backing file
|
|
backingFile, err := os.ReadFile(backingFilePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to read %s: %w", backingFilePath, err)
|
|
}
|
|
|
|
realDev = string(backingFile)
|
|
} else {
|
|
// Use dev as device
|
|
realDev = mountDev
|
|
}
|
|
|
|
break
|
|
}
|
|
|
|
if !isMounted {
|
|
realDev = dev["source"]
|
|
}
|
|
}
|
|
|
|
// Add labels
|
|
labels["device"] = realDev
|
|
labels["mountpoint"] = dev["path"]
|
|
|
|
fsType, err := linux.FSTypeToName(int32(statfs.Type))
|
|
if err == nil {
|
|
labels["fstype"] = fsType
|
|
}
|
|
|
|
// Add sample
|
|
statfsBsize := uint64(statfs.Bsize)
|
|
out.AddSamples(metrics.FilesystemSizeBytes, metrics.Sample{Value: float64(statfs.Blocks * statfsBsize), Labels: labels})
|
|
out.AddSamples(metrics.FilesystemAvailBytes, metrics.Sample{Value: float64(statfs.Bavail * statfsBsize), Labels: labels})
|
|
out.AddSamples(metrics.FilesystemFreeBytes, metrics.Sample{Value: float64(statfs.Bfree * statfsBsize), Labels: labels})
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func (d *lxc) loadRawLXCConfig(cc *liblxc.Container) error {
|
|
// Load the LXC raw config.
|
|
lxcConfig, ok := d.expandedConfig["raw.lxc"]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
// Write to temp config file.
|
|
f, err := os.CreateTemp("", "incus_config_")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = internalIO.WriteAll(f, []byte(lxcConfig))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = f.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Load the config.
|
|
err = cc.LoadConfigFile(f.Name())
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to load config file %q: %w", f.Name(), err)
|
|
}
|
|
|
|
_ = os.Remove(f.Name())
|
|
|
|
return nil
|
|
}
|
|
|
|
// forfileRunningLockName returns the forkfile-running_ID lock name.
|
|
func (d *common) forkfileRunningLockName() string {
|
|
return fmt.Sprintf("forkfile-running_%d", d.id)
|
|
}
|
|
|
|
// ReloadDevice triggers an empty Update call to the underlying device.
|
|
func (d *lxc) ReloadDevice(devName string) error {
|
|
dev, err := d.deviceLoad(d, devName, d.expandedDevices[devName])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return dev.Update(d.expandedDevices, true)
|
|
}
|
|
|
|
// CanLiveMigrate returns whether the container is live-migratable.
|
|
func (d *lxc) CanLiveMigrate() bool {
|
|
return util.IsTrue(d.expandedConfig["migration.stateful"])
|
|
}
|
|
|
|
// setupCredentials sets up the systemd credentials directory.
|
|
func (d *lxc) setupCredentials(update bool) error {
|
|
// Skip updating if the container isn't running.
|
|
if update && !d.IsRunning() {
|
|
return nil
|
|
}
|
|
|
|
credentialsDir := filepath.Join(d.Path(), "credentials")
|
|
credentials := map[string][]byte{}
|
|
oldCredentials := map[string]bool{}
|
|
|
|
var rootUID, rootGID int64
|
|
idmapset, err := d.NextIdmap()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if idmapset != nil {
|
|
rootUID, rootGID = idmapset.ShiftIntoNS(0, 0)
|
|
}
|
|
|
|
for k, v := range d.expandedConfig {
|
|
after, ok := strings.CutPrefix(k, "systemd.credential.")
|
|
if ok {
|
|
credentials[after] = []byte(v)
|
|
continue
|
|
}
|
|
|
|
after, ok = strings.CutPrefix(k, "systemd.credential-binary.")
|
|
if ok {
|
|
data, err := base64.RawStdEncoding.DecodeString(strings.TrimRight(v, "="))
|
|
if err != nil {
|
|
return fmt.Errorf("Invalid base64 value for %q: %q", k, v)
|
|
}
|
|
|
|
credentials[after] = data
|
|
}
|
|
}
|
|
|
|
// Cleanup the credentials directory.
|
|
if update && util.PathExists(credentialsDir) {
|
|
credEntries, err := os.ReadDir(credentialsDir)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to list credentials directory: %w", err)
|
|
}
|
|
|
|
for _, entry := range credEntries {
|
|
oldCredentials[entry.Name()] = true
|
|
}
|
|
} else {
|
|
_ = os.RemoveAll(credentialsDir)
|
|
|
|
err = internalUtil.MkdirAllOwner(credentialsDir, 0o100, int(rootUID), int(rootGID))
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to create credentials directory: %w", err)
|
|
}
|
|
}
|
|
|
|
credsRoot, err := os.OpenRoot(credentialsDir)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to open the credentials directory: %w", err)
|
|
}
|
|
|
|
defer func() { _ = credsRoot.Close() }()
|
|
|
|
for k, v := range credentials {
|
|
err := credsRoot.WriteFile(k, v, 0o400)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to write credential %q: %w", k, err)
|
|
}
|
|
|
|
err = credsRoot.Chown(k, int(rootUID), int(rootGID))
|
|
if err != nil {
|
|
return fmt.Errorf("Failed setting permissions for file %q: %w", k, err)
|
|
}
|
|
|
|
delete(oldCredentials, k)
|
|
}
|
|
|
|
for oldCredential := range oldCredentials {
|
|
err = os.Remove(filepath.Join(credentialsDir, oldCredential))
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to remove credential %q: %w", oldCredential, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GuestOS returns the guest OS. For containers, we can safely assume Linux.
|
|
func (d *lxc) GuestOS() string {
|
|
return "linux"
|
|
}
|
|
|
|
// CreateQcow2Snapshot creates a qcow2 snapshot for a running instance. Not supported by containers.
|
|
func (d *lxc) CreateQcow2Snapshot(devPath string, devName string, snapName string, backingFilename string) error {
|
|
return nil
|
|
}
|
|
|
|
// DeleteQcow2Snapshot deletes a qcow2 snapshot for a running instance. Not supported by containers.
|
|
func (d *lxc) DeleteQcow2Snapshot(devName string, snapshotIndex int, backingFilename string) error {
|
|
return nil
|
|
}
|
|
|
|
// ExportQcow2Block exports a qcow2 block device. Not supported by containers.
|
|
func (d *lxc) ExportQcow2Block(diskIndex int) (func(), string, error) {
|
|
return nil, "", nil
|
|
}
|
|
|
|
// setNICLink sets the link status of the given device.
|
|
func (d *lxc) setNICLink(devName string, connected bool, assumeUp bool) error {
|
|
// This check is added so that devices that cannot handle link states do not fail to initialize.
|
|
if connected && assumeUp {
|
|
return nil
|
|
}
|
|
|
|
link, err := ip.LinkByName(d.localConfig["volatile."+devName+".host_name"])
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to find interface %s: %w", devName, err)
|
|
}
|
|
|
|
if connected {
|
|
err = link.SetUp()
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to bring %s up: %w", devName, err)
|
|
}
|
|
} else {
|
|
err = link.SetDown()
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to bring %s down: %w", devName, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|