package shared import ( "errors" "fmt" "regexp" "strconv" "strings" "time" "github.com/lxc/lxd/lxd/instance/instancetype" "github.com/lxc/lxd/shared/units" "github.com/lxc/lxd/shared/validate" ) // InstanceAction indicates the type of action being performed. type InstanceAction string // InstanceAction types. const ( Stop InstanceAction = "stop" Start InstanceAction = "start" Restart InstanceAction = "restart" Freeze InstanceAction = "freeze" Unfreeze InstanceAction = "unfreeze" ) // ConfigVolatilePrefix indicates the prefix used for volatile config keys. const ConfigVolatilePrefix = "volatile." // IsRootDiskDevice returns true if the given device representation is configured as root disk for // an instance. It typically get passed a specific entry of api.Instance.Devices. func IsRootDiskDevice(device map[string]string) bool { // Root disk devices also need a non-empty "pool" property, but we can't check that here // because this function is used with clients talking to older servers where there was no // concept of a storage pool, and also it is used for migrating from old to new servers. // The validation of the non-empty "pool" property is done inside the disk device itself. if device["type"] == "disk" && device["path"] == "/" && device["source"] == "" { return true } return false } // ErrNoRootDisk means there is no root disk device found. var ErrNoRootDisk = fmt.Errorf("No root device could be found") // GetRootDiskDevice returns the instance device that is configured as root disk. // Returns the device name and device config map. func GetRootDiskDevice(devices map[string]map[string]string) (string, map[string]string, error) { var devName string var dev map[string]string for n, d := range devices { if IsRootDiskDevice(d) { if devName != "" { return "", nil, fmt.Errorf("More than one root device found") } devName = n dev = d } } if devName != "" { return devName, dev, nil } return "", nil, ErrNoRootDisk } // HugePageSizeKeys is a list of known hugepage size configuration keys. var HugePageSizeKeys = [...]string{"limits.hugepages.64KB", "limits.hugepages.1MB", "limits.hugepages.2MB", "limits.hugepages.1GB"} // HugePageSizeSuffix contains the list of known hugepage size suffixes. var HugePageSizeSuffix = [...]string{"64KB", "1MB", "2MB", "1GB"} // InstanceConfigKeysAny is a map of config key to validator. (keys applying to containers AND virtual machines) var InstanceConfigKeysAny = map[string]func(value string) error{ "boot.autostart": validate.Optional(validate.IsBool), "boot.autostart.delay": validate.Optional(validate.IsInt64), "boot.autostart.priority": validate.Optional(validate.IsInt64), "boot.stop.priority": validate.Optional(validate.IsInt64), "boot.host_shutdown_timeout": validate.Optional(validate.IsInt64), "cloud-init.network-config": validate.Optional(validate.IsAny), "cloud-init.user-data": validate.Optional(validate.IsAny), "cloud-init.vendor-data": validate.Optional(validate.IsAny), "cluster.evacuate": validate.Optional(validate.IsOneOf("auto", "migrate", "live-migrate", "stop")), "limits.cpu": func(value string) error { if value == "" { return nil } // Validate the character set match, _ := regexp.MatchString("^[-,0-9]*$", value) if !match { return fmt.Errorf("Invalid CPU limit syntax") } // Validate first character if strings.HasPrefix(value, "-") || strings.HasPrefix(value, ",") { return fmt.Errorf("CPU limit can't start with a separator") } // Validate last character if strings.HasSuffix(value, "-") || strings.HasSuffix(value, ",") { return fmt.Errorf("CPU limit can't end with a separator") } return nil }, "limits.disk.priority": validate.Optional(validate.IsPriority), "limits.memory": func(value string) error { if value == "" { return nil } if strings.HasSuffix(value, "%") { num, err := strconv.ParseInt(strings.TrimSuffix(value, "%"), 10, 64) if err != nil { return err } if num == 0 { return errors.New("Memory limit can't be 0%") } return nil } num, err := units.ParseByteSizeString(value) if err != nil { return err } if num == 0 { return fmt.Errorf("Memory limit can't be 0") } return nil }, "limits.network.priority": validate.Optional(validate.IsPriority), // Caller is responsible for full validation of any raw.* value. "raw.apparmor": validate.IsAny, "security.devlxd": validate.Optional(validate.IsBool), "security.protection.delete": validate.Optional(validate.IsBool), "snapshots.schedule": validate.Optional(validate.IsCron([]string{"@hourly", "@daily", "@midnight", "@weekly", "@monthly", "@annually", "@yearly", "@startup", "@never"})), "snapshots.schedule.stopped": validate.Optional(validate.IsBool), "snapshots.pattern": validate.IsAny, "snapshots.expiry": func(value string) error { // Validate expression _, err := GetSnapshotExpiry(time.Time{}, value) return err }, // Volatile keys. "volatile.apply_template": validate.IsAny, "volatile.base_image": validate.IsAny, "volatile.cloud-init.instance-id": validate.Optional(validate.IsUUID), "volatile.evacuate.origin": validate.IsAny, "volatile.last_state.idmap": validate.IsAny, "volatile.last_state.power": validate.IsAny, "volatile.idmap.base": validate.IsAny, "volatile.idmap.current": validate.IsAny, "volatile.idmap.next": validate.IsAny, "volatile.apply_quota": validate.IsAny, "volatile.uuid": validate.Optional(validate.IsUUID), "volatile.vsock_id": validate.Optional(validate.IsInt64), // Caller is responsible for full validation of any raw.* value. "raw.idmap": validate.IsAny, } // InstanceConfigKeysContainer is a map of config key to validator. (keys applying to containers only) var InstanceConfigKeysContainer = map[string]func(value string) error{ "limits.cpu.allowance": func(value string) error { if value == "" { return nil } if strings.HasSuffix(value, "%") { // Percentage based allocation _, err := strconv.Atoi(strings.TrimSuffix(value, "%")) if err != nil { return err } return nil } // Time based allocation fields := strings.SplitN(value, "/", 2) if len(fields) != 2 { return fmt.Errorf("Invalid allowance: %s", value) } _, err := strconv.Atoi(strings.TrimSuffix(fields[0], "ms")) if err != nil { return err } _, err = strconv.Atoi(strings.TrimSuffix(fields[1], "ms")) if err != nil { return err } return nil }, "limits.cpu.priority": validate.Optional(validate.IsPriority), "limits.hugepages.64KB": validate.Optional(validate.IsSize), "limits.hugepages.1MB": validate.Optional(validate.IsSize), "limits.hugepages.2MB": validate.Optional(validate.IsSize), "limits.hugepages.1GB": validate.Optional(validate.IsSize), "limits.memory.enforce": validate.Optional(validate.IsOneOf("soft", "hard")), "limits.memory.swap": validate.Optional(validate.IsBool), "limits.memory.swap.priority": validate.Optional(validate.IsPriority), "limits.processes": validate.Optional(validate.IsInt64), "linux.kernel_modules": validate.IsAny, "migration.incremental.memory": validate.Optional(validate.IsBool), "migration.incremental.memory.iterations": validate.Optional(validate.IsUint32), "migration.incremental.memory.goal": validate.Optional(validate.IsUint32), "nvidia.runtime": validate.Optional(validate.IsBool), "nvidia.driver.capabilities": validate.IsAny, "nvidia.require.cuda": validate.IsAny, "nvidia.require.driver": validate.IsAny, // Caller is responsible for full validation of any raw.* value. "raw.lxc": validate.IsAny, "raw.seccomp": validate.IsAny, "security.devlxd.images": validate.Optional(validate.IsBool), "security.idmap.base": validate.Optional(validate.IsUint32), "security.idmap.isolated": validate.Optional(validate.IsBool), "security.idmap.size": validate.Optional(validate.IsUint32), "security.nesting": validate.Optional(validate.IsBool), "security.privileged": validate.Optional(validate.IsBool), "security.protection.shift": validate.Optional(validate.IsBool), "security.syscalls.allow": validate.IsAny, "security.syscalls.blacklist_default": validate.Optional(validate.IsBool), "security.syscalls.blacklist_compat": validate.Optional(validate.IsBool), "security.syscalls.blacklist": validate.IsAny, "security.syscalls.deny_default": validate.Optional(validate.IsBool), "security.syscalls.deny_compat": validate.Optional(validate.IsBool), "security.syscalls.deny": validate.IsAny, "security.syscalls.intercept.bpf": validate.Optional(validate.IsBool), "security.syscalls.intercept.bpf.devices": validate.Optional(validate.IsBool), "security.syscalls.intercept.mknod": validate.Optional(validate.IsBool), "security.syscalls.intercept.mount": validate.Optional(validate.IsBool), "security.syscalls.intercept.mount.allowed": validate.IsAny, "security.syscalls.intercept.mount.fuse": validate.IsAny, "security.syscalls.intercept.mount.shift": validate.Optional(validate.IsBool), "security.syscalls.intercept.sched_setscheduler": validate.Optional(validate.IsBool), "security.syscalls.intercept.setxattr": validate.Optional(validate.IsBool), "security.syscalls.whitelist": validate.IsAny, } // InstanceConfigKeysVM is a map of config key to validator. (keys applying to VM only) var InstanceConfigKeysVM = map[string]func(value string) error{ "limits.memory.hugepages": validate.Optional(validate.IsBool), "migration.stateful": validate.Optional(validate.IsBool), // Caller is responsible for full validation of any raw.* value. "raw.qemu": validate.IsAny, "security.agent.metrics": validate.Optional(validate.IsBool), "security.secureboot": validate.Optional(validate.IsBool), "agent.nic_config": validate.Optional(validate.IsBool), } // ConfigKeyChecker returns a function that will check whether or not // a provide value is valid for the associate config key. Returns an // error if the key is not known. The checker function only performs // syntactic checking of the value, semantic and usage checking must // be done by the caller. User defined keys are always considered to // be valid, e.g. user.* and environment.* keys. func ConfigKeyChecker(key string, instanceType instancetype.Type) (func(value string) error, error) { if f, ok := InstanceConfigKeysAny[key]; ok { return f, nil } if instanceType == instancetype.Any || instanceType == instancetype.Container { if f, ok := InstanceConfigKeysContainer[key]; ok { return f, nil } } if instanceType == instancetype.Any || instanceType == instancetype.VM { if f, ok := InstanceConfigKeysVM[key]; ok { return f, nil } } if strings.HasPrefix(key, ConfigVolatilePrefix) { if strings.HasSuffix(key, ".hwaddr") { return validate.IsAny, nil } if strings.HasSuffix(key, ".name") { return validate.IsAny, nil } if strings.HasSuffix(key, ".host_name") { return validate.IsAny, nil } if strings.HasSuffix(key, ".mtu") { return validate.IsAny, nil } if strings.HasSuffix(key, ".created") { return validate.IsAny, nil } if strings.HasSuffix(key, ".id") { return validate.IsAny, nil } if strings.HasSuffix(key, ".vlan") { return validate.IsAny, nil } if strings.HasSuffix(key, ".spoofcheck") { return validate.IsAny, nil } if strings.HasSuffix(key, ".last_state.vf.parent") { return validate.IsAny, nil } if strings.HasSuffix(key, ".apply_quota") { return validate.IsAny, nil } if strings.HasSuffix(key, ".ceph_rbd") { return validate.IsAny, nil } if strings.HasSuffix(key, ".driver") { return validate.IsAny, nil } if strings.HasSuffix(key, ".uuid") { return validate.IsAny, nil } } if strings.HasPrefix(key, "environment.") { return validate.IsAny, nil } if strings.HasPrefix(key, "user.") { return validate.IsAny, nil } if strings.HasPrefix(key, "image.") { return validate.IsAny, nil } if strings.HasPrefix(key, "limits.kernel.") && (len(key) > len("limits.kernel.")) { return validate.IsAny, nil } if (instanceType == instancetype.Any || instanceType == instancetype.Container) && strings.HasPrefix(key, "linux.sysctl.") { return validate.IsAny, nil } return nil, fmt.Errorf("Unknown configuration key: %s", key) } // InstanceGetParentAndSnapshotName returns the parent instance name, snapshot name, // and whether it actually was a snapshot name. func InstanceGetParentAndSnapshotName(name string) (string, string, bool) { fields := strings.SplitN(name, SnapshotDelimiter, 2) if len(fields) == 1 { return name, "", false } return fields[0], fields[1], true } // InstanceIncludeWhenCopying is used to decide whether to include a config item or not when copying an instance. // The remoteCopy argument indicates if the copy is remote (i.e between LXD nodes) as this affects the keys kept. func InstanceIncludeWhenCopying(configKey string, remoteCopy bool) bool { if configKey == "volatile.base_image" { return true // Include volatile.base_image always as it can help optimize copies. } if configKey == "volatile.last_state.idmap" && !remoteCopy { return true // Include volatile.last_state.idmap when doing local copy to avoid needless remapping. } if strings.HasPrefix(configKey, ConfigVolatilePrefix) { return false // Exclude all other volatile keys. } return true // Keep all other keys. }