Skip to content

Commit

Permalink
client,daemon: expose features supported/enabled in /v2/system-info
Browse files Browse the repository at this point in the history
Add a "features" field containing a map of feature flag names to boolean
subfields for whether the feature is "supported" and/or "enabled", along
with an "unsupported-reason" if the feature is not supported.

Feature flags which are not set to true or false are omitted from this
map. Feature flags may be unsupported but nonetheless enabled. This
indicates that the feature flag has been set to true, but the backing
feature itself is not currently supported.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
  • Loading branch information
olivercalder committed Mar 13, 2024
1 parent c653e2b commit 370eec0
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 14 deletions.
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* Added API route for creating recovery systems: POST to `/v2/systems` with action `create`
* Added API route for removing recovery systems: POST to `/v2/systems/{label}` with action `remove`
* Support for user daemons by introducing new control switches --user/--system/--users for service start/stop/restart
* client,daemon: expose features supported/enabled in `/v2/system-info`

# New in snapd 2.61.3:
* Install systemd files in correct location for 24.04
Expand Down
5 changes: 4 additions & 1 deletion client/client.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2015-2020 Canonical Ltd
* Copyright (C) 2015-2024 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
Expand Down Expand Up @@ -36,6 +36,7 @@ import (
"time"

"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/features"
"github.com/snapcore/snapd/httputil"
"github.com/snapcore/snapd/jsonutil"
)
Expand Down Expand Up @@ -688,6 +689,8 @@ type SysInfo struct {
Refresh RefreshInfo `json:"refresh,omitempty"`
Confinement string `json:"confinement"`
SandboxFeatures map[string][]string `json:"sandbox-features,omitempty"`

Features map[string]features.FeatureInfo `json:"features,omitempty"`
}

func (rsp *response) err(cli *Client, statusCode int) error {
Expand Down
39 changes: 28 additions & 11 deletions client/client_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2015-2016 Canonical Ltd
* Copyright (C) 2015-2024 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
Expand Down Expand Up @@ -39,6 +39,7 @@ import (

"github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/features"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/testutil"
)
Expand Down Expand Up @@ -350,16 +351,26 @@ func (cs *clientSuite) TestClientWhoAmISomebody(c *C) {
}

func (cs *clientSuite) TestClientSysInfo(c *C) {
cs.rsp = `{"type": "sync", "result":
{"series": "16",
"version": "2",
"os-release": {"id": "ubuntu", "version-id": "16.04"},
"on-classic": true,
"build-id": "1234",
"confinement": "strict",
"architecture": "TI-99/4A",
"virtualization": "MESS",
"sandbox-features": {"backend": ["feature-1", "feature-2"]}}}`
cs.rsp = `{
"type": "sync",
"result": {
"series": "16",
"version": "2",
"os-release": {"id": "ubuntu", "version-id": "16.04"},
"on-classic": true,
"build-id": "1234",
"confinement": "strict",
"architecture": "TI-99/4A",
"virtualization": "MESS",
"sandbox-features": {"backend": ["feature-1", "feature-2"]},
"features": {
"foo": {"supported": false, "unsupported-reason": "too foo", "enabled": false},
"bar": {"supported": false, "unsupported-reason": "not bar enough", "enabled": true},
"baz": {"supported": true, "enabled": false},
"buzz": {"supported": true, "enabled": true}
}
}
}`
sysInfo, err := cs.cli.SysInfo()
c.Check(err, IsNil)
c.Check(sysInfo, DeepEquals, &client.SysInfo{
Expand All @@ -377,6 +388,12 @@ func (cs *clientSuite) TestClientSysInfo(c *C) {
BuildID: "1234",
Architecture: "TI-99/4A",
Virtualization: "MESS",
Features: map[string]features.FeatureInfo{
"foo": {Supported: false, UnsupportedReason: "too foo", Enabled: false},
"bar": {Supported: false, UnsupportedReason: "not bar enough", Enabled: true},
"baz": {Supported: true, Enabled: false},
"buzz": {Supported: true, Enabled: true},
},
})
}

Expand Down
6 changes: 5 additions & 1 deletion daemon/api_general.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2015-2020 Canonical Ltd
* Copyright (C) 2015-2024 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
Expand Down Expand Up @@ -30,10 +30,12 @@ import (
"github.com/snapcore/snapd/arch"
"github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/features"
"github.com/snapcore/snapd/interfaces"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/overlord/configstate/config"
"github.com/snapcore/snapd/overlord/devicestate"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/release"
Expand Down Expand Up @@ -105,6 +107,7 @@ func sysInfo(c *Command, r *http.Request, user *auth.UserState) Response {
deviceMgr := c.d.overlord.DeviceManager()
st.Lock()
defer st.Unlock()
tr := config.NewTransaction(st)
nextRefresh := snapMgr.NextRefresh()
lastRefresh, _ := snapMgr.LastRefresh()
refreshHold, _ := snapMgr.EffectiveRefreshHold()
Expand Down Expand Up @@ -143,6 +146,7 @@ func sysInfo(c *Command, r *http.Request, user *auth.UserState) Response {
"refresh": refreshInfo,
"architecture": arch.DpkgArchitecture(),
"system-mode": deviceMgr.SystemMode(devicestate.SysAny),
"features": features.All(tr),
}
if systemdVirt != "" {
m["virtualization"] = systemdVirt
Expand Down
54 changes: 53 additions & 1 deletion daemon/api_general_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2014-2020 Canonical Ltd
* Copyright (C) 2014-2024 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
Expand Down Expand Up @@ -35,12 +35,14 @@ import (
"github.com/snapcore/snapd/boot"
"github.com/snapcore/snapd/daemon"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/features"
"github.com/snapcore/snapd/interfaces/ifacetest"
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/overlord/configstate/config"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/sandbox"
"github.com/snapcore/snapd/systemd"
)

var _ = check.Suite(&generalSuite{})
Expand Down Expand Up @@ -90,6 +92,8 @@ func (s *generalSuite) TestSysInfo(c *check.C) {
tr := config.NewTransaction(st)
tr.Set("core", "refresh.schedule", "00:00-9:00/12:00-13:00")
tr.Set("core", "refresh.timer", "8:00~9:00/2")
tr.Set("core", "experimental.parallel-instances", "false")
tr.Set("core", "experimental.quota-groups", "true")
tr.Commit()
st.Unlock()

Expand All @@ -103,6 +107,9 @@ func (s *generalSuite) TestSysInfo(c *check.C) {
dirs.SetRootDir(dirs.GlobalRootDir)
restore = daemon.MockSystemdVirt("magic")
defer restore()
// Set systemd version <230 so QuotaGroups feature unsupported
restore = systemd.MockSystemdVersion(229, nil)
defer restore()

buildID := "this-is-my-build-id"
restore = daemon.MockBuildID(buildID)
Expand Down Expand Up @@ -145,7 +152,48 @@ func (s *generalSuite) TestSysInfo(c *check.C) {
const kernelVersionKey = "kernel-version"
c.Check(rsp.Result.(map[string]interface{})[kernelVersionKey], check.Not(check.Equals), "")
delete(rsp.Result.(map[string]interface{}), kernelVersionKey)
// Extract "features" field and remove it from result; check it later.
const featuresKey = "features"
resultFeatures := rsp.Result.(map[string]interface{})[featuresKey]
c.Check(resultFeatures, check.Not(check.Equals), "")
delete(rsp.Result.(map[string]interface{}), featuresKey)

c.Check(rsp.Result, check.DeepEquals, expected)

// Check that "features" is map
featuresAll, ok := resultFeatures.(map[string]interface{})
c.Assert(ok, check.Equals, true)
// Ensure that Layouts exists and is feature.FeatureInfo
layoutsInfoRaw, exists := featuresAll[features.Layouts.String()]
c.Assert(exists, check.Equals, true)
layoutsInfo, ok := layoutsInfoRaw.(map[string]interface{})
c.Assert(ok, check.Equals, true, check.Commentf("%+v", layoutsInfoRaw))
// Ensure that Layouts is supported and enabled
c.Check(layoutsInfo["supported"], check.Equals, true)
_, exists = layoutsInfo["unsupported-reason"]
c.Check(exists, check.Equals, false)
c.Check(layoutsInfo["enabled"], check.Equals, true)
// Ensure that ParallelInstances exists and is a feature.FeatureInfo
parallelInstancesInfoRaw, exists := featuresAll[features.ParallelInstances.String()]
c.Assert(exists, check.Equals, true)
parallelInstancesInfo, ok := parallelInstancesInfoRaw.(map[string]interface{})
c.Assert(ok, check.Equals, true)
// Ensure that ParallelInstances is supported and not enabled
c.Check(parallelInstancesInfo["supported"], check.Equals, true)
_, exists = parallelInstancesInfo["unsupported-reason"]
c.Check(exists, check.Equals, false)
c.Check(parallelInstancesInfo["enabled"], check.Equals, false)
// Ensure that QuotaGroups exists and is a feature.FeatureInfo
quotaGroupsInfoRaw, exists := featuresAll[features.QuotaGroups.String()]
c.Assert(exists, check.Equals, true)
quotaGroupsInfo, ok := quotaGroupsInfoRaw.(map[string]interface{})
c.Assert(ok, check.Equals, true)
// Ensure that QuotaGroups is unsupported but enabled
c.Check(quotaGroupsInfo["supported"], check.Equals, false)
unsupportedReason, exists := quotaGroupsInfo["unsupported-reason"]
c.Check(exists, check.Equals, true)
c.Check(unsupportedReason, check.Not(check.Equals), "")
c.Check(quotaGroupsInfo["enabled"], check.Equals, true)
}

func (s *generalSuite) TestSysInfoLegacyRefresh(c *check.C) {
Expand Down Expand Up @@ -224,6 +272,8 @@ func (s *generalSuite) TestSysInfoLegacyRefresh(c *check.C) {
c.Check(rsp.Type, check.Equals, daemon.ResponseTypeSync)
const kernelVersionKey = "kernel-version"
delete(rsp.Result.(map[string]interface{}), kernelVersionKey)
const featuresKey = "features"
delete(rsp.Result.(map[string]interface{}), featuresKey)
c.Check(rsp.Result, check.DeepEquals, expected)
}

Expand Down Expand Up @@ -302,6 +352,8 @@ func (s *generalSuite) testSysInfoSystemMode(c *check.C, mode string) {
c.Check(rsp.Type, check.Equals, daemon.ResponseTypeSync)
const kernelVersionKey = "kernel-version"
delete(rsp.Result.(map[string]interface{}), kernelVersionKey)
const featuresKey = "features"
delete(rsp.Result.(map[string]interface{}), featuresKey)
c.Check(rsp.Result, check.DeepEquals, expected)
}

Expand Down

0 comments on commit 370eec0

Please sign in to comment.