diff --git a/doc/api-extensions.md b/doc/api-extensions.md index b58b23749db..42b1840b934 100644 --- a/doc/api-extensions.md +++ b/doc/api-extensions.md @@ -2748,3 +2748,7 @@ Adds `acme.http.port` to control an alternative HTTP port for `HTTP-01` validati ## `network_ovn_ipv4_dhcp_expiry` Introduces `ipv4.dhcp.expiry` for OVN networks. + +## `instance_state_cpu_time` + +This adds an `allocated_time` field below `CPU` in the instance state API. diff --git a/doc/rest-api.yaml b/doc/rest-api.yaml index 313b9c96f2b..12c0ccedbc1 100644 --- a/doc/rest-api.yaml +++ b/doc/rest-api.yaml @@ -2306,6 +2306,12 @@ definitions: x-go-package: github.com/lxc/incus/v6/shared/api InstanceStateCPU: properties: + allocated_time: + description: CPU time available per second, in nanoseconds + example: 4000000000 + format: int64 + type: integer + x-go-name: AllocatedTime usage: description: CPU usage in nanoseconds example: 3637691016 diff --git a/internal/server/cgroup/abstraction.go b/internal/server/cgroup/abstraction.go index b71cb21a686..85a083bec74 100644 --- a/internal/server/cgroup/abstraction.go +++ b/internal/server/cgroup/abstraction.go @@ -749,6 +749,65 @@ func (cg *CGroup) SetCPUCfsLimit(limitPeriod int64, limitQuota int64) error { return ErrUnknownVersion } +// GetCPUCfsLimit gets the quota and duration in ms for each scheduling period. +func (cg *CGroup) GetCPUCfsLimit() (int64, int64, error) { + version := cgControllers["cpu"] + switch version { + case Unavailable: + return -1, -1, ErrControllerMissing + case V1: + limitQuotaStr, err := cg.rw.Get(version, "cpu", "cpu.cfs_quota_us") + if err != nil { + return -1, -1, err + } + + limitQuota, err := strconv.ParseInt(limitQuotaStr, 10, 64) + if err != nil { + return -1, -1, err + } + + limitPeriodStr, err := cg.rw.Get(version, "cpu", "cpu.cfs_period_us") + if err != nil { + return -1, -1, err + } + + limitPeriod, err := strconv.ParseInt(limitPeriodStr, 10, 64) + if err != nil { + return -1, -1, err + } + + return limitPeriod, limitQuota, nil + case V2: + cpuMax, err := cg.rw.Get(version, "cpu", "cpu.max") + if err != nil { + return -1, -1, err + } + + cpuMaxFields := strings.Split(cpuMax, " ") + if len(cpuMaxFields) != 2 { + return -1, -1, errors.New("Couldn't parse CFS limits") + } + + if cpuMaxFields[0] == "max" { + return -1, -1, nil + } + + limitQuota, err := strconv.ParseInt(cpuMaxFields[0], 10, 64) + if err != nil { + return -1, -1, err + } + + limitPeriod, err := strconv.ParseInt(cpuMaxFields[1], 10, 64) + if err != nil { + return -1, -1, err + } + + return limitPeriod, limitQuota, nil + } + + return -1, -1, ErrUnknownVersion +} + // SetHugepagesLimit applies a limit to the number of processes. func (cg *CGroup) SetHugepagesLimit(pageType string, limit int64) error { version := cgControllers["hugetlb"] diff --git a/internal/server/instance/drivers/driver_lxc.go b/internal/server/instance/drivers/driver_lxc.go index 17ff6c5362a..3a70d576c21 100644 --- a/internal/server/instance/drivers/driver_lxc.go +++ b/internal/server/instance/drivers/driver_lxc.go @@ -7521,6 +7521,19 @@ func (d *lxc) Exec(req api.InstanceExecPost, stdin *os.File, stdout *os.File, st 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{} @@ -7529,23 +7542,31 @@ func (d *lxc) cpuState() api.InstanceStateCPU { return cpu } - // CPU usage in seconds cg, err := d.cgroup(cc, true) if err != nil { return cpu } - if !d.state.OS.CGInfo.Supports(cgroup.CPUAcct, cg) { + cpuUsage, ok := d.cpuStateUsage(cg) + if ok { + cpu.Usage = cpuUsage + } + + cpuCount, err := cg.GetEffectiveCPUs() + if err != nil { return cpu } - value, err := cg.GetCPUAcctUsage() + limitPeriod, limitQuota, err := cg.GetCPUCfsLimit() if err != nil { - cpu.Usage = -1 return cpu } - cpu.Usage = value + if limitQuota == -1 { + cpu.AllocatedTime = int64(cpuCount) * 1_000_000_000 + } else { + cpu.AllocatedTime = 1_000_000_000 * limitQuota / limitPeriod + } return cpu } diff --git a/internal/server/instance/drivers/driver_qemu.go b/internal/server/instance/drivers/driver_qemu.go index 1b62b460a6b..fa5056751a9 100644 --- a/internal/server/instance/drivers/driver_qemu.go +++ b/internal/server/instance/drivers/driver_qemu.go @@ -8138,6 +8138,17 @@ func (d *qemu) renderState(statusCode api.StatusCode) (*api.InstanceState, error } } + // Populate the CPU time allocation + limitsCPU, ok := d.expandedConfig["limits.cpu"] + if ok { + cpuCount, err := strconv.ParseInt(limitsCPU, 10, 64) + if err != nil { + status.CPU.AllocatedTime = cpuCount * 1_000_000_000 + } + } else { + status.CPU.AllocatedTime = qemudefault.CPUCores * 1_000_000_000 + } + // Populate host_name for network devices. for k, m := range d.ExpandedDevices() { // We only care about nics. diff --git a/internal/version/api.go b/internal/version/api.go index 8a11fa8b702..f76c62fca45 100644 --- a/internal/version/api.go +++ b/internal/version/api.go @@ -471,6 +471,7 @@ var APIExtensions = []string{ "network_dns_nameservers", "acme_http01_port", "network_ovn_ipv4_dhcp_expiry", + "instance_state_cpu_time", } // APIExtensionsCount returns the number of available API extensions. diff --git a/shared/api/instance_state.go b/shared/api/instance_state.go index 08bd3127bcf..b28565c81a5 100644 --- a/shared/api/instance_state.go +++ b/shared/api/instance_state.go @@ -98,6 +98,12 @@ type InstanceStateCPU struct { // CPU usage in nanoseconds // Example: 3637691016 Usage int64 `json:"usage" yaml:"usage"` + + // CPU time available per second, in nanoseconds + // Example: 4000000000 + // + // API extension: instance_state_cpu_time + AllocatedTime int64 `json:"allocated_time" yaml:"allocated_time"` } // InstanceStateMemory represents the memory information section of an instance's state. diff --git a/test/suites/basic.sh b/test/suites/basic.sh index 747fab05f88..fbe465427a5 100644 --- a/test/suites/basic.sh +++ b/test/suites/basic.sh @@ -399,6 +399,18 @@ test_basic_usage() { [ "$(incus config get test-limits limits.cpu)" = "1" ] [ "$(incus config get test-limits limits.cpu.allowance)" = "50%" ] [ "$(incus config get test-limits limits.memory)" = "204MiB" ] + + # Test CPU allocation information + [ "$(incus query /1.0/instances/test-limits/state | jq -r '.cpu.allocated_time')" = "1000000000" ] + incus config set test-limits limits.cpu.allowance 100ms/200ms + [ "$(incus query /1.0/instances/test-limits/state | jq -r '.cpu.allocated_time')" = "500000000" ] + incus config set test-limits limits.cpu 2 + [ "$(incus query /1.0/instances/test-limits/state | jq -r '.cpu.allocated_time')" = "500000000" ] + incus config unset test-limits limits.cpu.allowance + [ "$(incus query /1.0/instances/test-limits/state | jq -r '.cpu.allocated_time')" = "2000000000" ] + incus config unset test-limits limits.cpu + [ "$(incus query /1.0/instances/test-limits/state | jq -r '.cpu.allocated_time')" = "$(nproc)000000000" ] + incus delete -f test-limits # Test last_used_at field is working properly