mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
* o/assertstate, o/devicestate: add more general function for fetching validation set assertions * daemon, client: add API routes for creating/removing recovery system * daemon, o/snapstate: add .snap file extension to snaps from forms The seed writer will fail to consider files as snaps if their filenames do not end in .snap. * tests: test creating a recovery system * tests: add spread test for offline creation of recovery system * tests: update offline recovery system test to reboot into new system * tests/nested/manual/recovery-system-reboot: add variants for factory-reset and install modes * tests: replace usage of default-recovery-system with default-recovery * o/devicestate: enable offline creation of recovery system entirely from pre-installed snaps * daemon, client: test that offline API works without providing snaps or validation sets * tests/nested/manual/recovery-system-offline: test offline remodel with only pre-installed snaps * tests/nested/manual/recovery-system-reboot: modify test to create system with new set of essential snaps * tests: disable shellcheck printf check * daemon: rename functions for working with form values and add one for working with booleans * daemon: acquire state lock later in postSystemActionCreateOffline * daemon: cleanup form files if we fail to make change to create a recovery system * daemon: rename parseValidationSets to assertionsFromValidationSetStrings for clarity * client, daemon, tests: add "offline" field to create recovery system JSON api * daemon: convert TODO about comma-delimited list into explanation of why we use a comma delimited list * NEWS.md: add mention of create/remove recovery systems API * tests/nested/manual/recovery-system-offline: explicitly disable network from nested vm * tests/nested/manual/recovery-system-reboot: do not use new gadget in recovery system for now * tests/lib/nested.sh: add variable NESTED_FORCE_MS_KEYS to force using microsoft keys * tests/nested/manual/recovery-system-reboot: add back gadget snap swap to test * tests/nested/manual/recovery-system-reboot: retry POST to remove since there might be an auto-refresh happening
270 lines
8.8 KiB
Go
270 lines
8.8 KiB
Go
// -*- Mode: Go; indent-tabs-mode: t -*-
|
|
|
|
/*
|
|
* Copyright (C) 2020-2023 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
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/snapcore/snapd/gadget"
|
|
"github.com/snapcore/snapd/snap"
|
|
)
|
|
|
|
// SystemModelData contains information about the model
|
|
type SystemModelData struct {
|
|
// Model as the model assertion
|
|
Model string `json:"model,omitempty"`
|
|
// BrandID corresponds to brand-id in the model assertion
|
|
BrandID string `json:"brand-id,omitempty"`
|
|
// DisplayName is human friendly name, corresponds to display-name in
|
|
// the model assertion
|
|
DisplayName string `json:"display-name,omitempty"`
|
|
}
|
|
|
|
type System struct {
|
|
// Current is true when the system running now was installed from that
|
|
// recovery seed
|
|
Current bool `json:"current,omitempty"`
|
|
// Label of the recovery system
|
|
Label string `json:"label,omitempty"`
|
|
// Model information
|
|
Model SystemModelData `json:"model,omitempty"`
|
|
// Brand information
|
|
Brand snap.StoreAccount `json:"brand,omitempty"`
|
|
// Actions available for this system
|
|
Actions []SystemAction `json:"actions,omitempty"`
|
|
// DefaultRecoverySystem is true when the system is the default recovery system
|
|
DefaultRecoverySystem bool `json:"default-recovery-system,omitempty"`
|
|
}
|
|
|
|
type SystemAction struct {
|
|
// Title is a user presentable action description
|
|
Title string `json:"title,omitempty"`
|
|
// Mode given action can be executed in
|
|
Mode string `json:"mode,omitempty"`
|
|
}
|
|
|
|
// ListSystems list all systems available for seeding or recovery.
|
|
func (client *Client) ListSystems() ([]System, error) {
|
|
type systemsResponse struct {
|
|
Systems []System `json:"systems,omitempty"`
|
|
}
|
|
|
|
var rsp systemsResponse
|
|
|
|
if _, err := client.doSync("GET", "/v2/systems", nil, nil, nil, &rsp); err != nil {
|
|
return nil, xerrors.Errorf("cannot list recovery systems: %v", err)
|
|
}
|
|
return rsp.Systems, nil
|
|
}
|
|
|
|
// DoSystemAction issues a request to perform an action using the given seed
|
|
// system and its mode.
|
|
func (client *Client) DoSystemAction(systemLabel string, action *SystemAction) error {
|
|
if systemLabel == "" {
|
|
return fmt.Errorf("cannot request an action without the system")
|
|
}
|
|
if action == nil {
|
|
return fmt.Errorf("cannot request an action without one")
|
|
}
|
|
// deeper verification is done by the backend
|
|
|
|
req := struct {
|
|
Action string `json:"action"`
|
|
*SystemAction
|
|
}{
|
|
Action: "do",
|
|
SystemAction: action,
|
|
}
|
|
|
|
var body bytes.Buffer
|
|
if err := json.NewEncoder(&body).Encode(&req); err != nil {
|
|
return err
|
|
}
|
|
if _, err := client.doSync("POST", "/v2/systems/"+systemLabel, nil, nil, &body, nil); err != nil {
|
|
return xerrors.Errorf("cannot request system action: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RebootToSystem issues a request to reboot into system with the
|
|
// given label and the given mode.
|
|
//
|
|
// When called without a systemLabel and without a mode it will just
|
|
// trigger a regular reboot.
|
|
//
|
|
// When called without a systemLabel but with a mode it will use
|
|
// the current system to enter the given mode.
|
|
//
|
|
// Note that "recover" and "run" modes are only available for the
|
|
// current system.
|
|
func (client *Client) RebootToSystem(systemLabel, mode string) error {
|
|
// verification is done by the backend
|
|
|
|
req := struct {
|
|
Action string `json:"action"`
|
|
Mode string `json:"mode"`
|
|
}{
|
|
Action: "reboot",
|
|
Mode: mode,
|
|
}
|
|
|
|
var body bytes.Buffer
|
|
if err := json.NewEncoder(&body).Encode(&req); err != nil {
|
|
return err
|
|
}
|
|
if _, err := client.doSync("POST", "/v2/systems/"+systemLabel, nil, nil, &body, nil); err != nil {
|
|
if systemLabel != "" {
|
|
return xerrors.Errorf("cannot request system reboot into %q: %v", systemLabel, err)
|
|
}
|
|
return xerrors.Errorf("cannot request system reboot: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type StorageEncryptionSupport string
|
|
|
|
const (
|
|
// forcefull disabled by the device
|
|
StorageEncryptionSupportDisabled = "disabled"
|
|
// encryption available and usable
|
|
StorageEncryptionSupportAvailable = "available"
|
|
// encryption unavailable but not required
|
|
StorageEncryptionSupportUnavailable = "unavailable"
|
|
// encryption unavailable and required, this is an error
|
|
StorageEncryptionSupportDefective = "defective"
|
|
)
|
|
|
|
type StorageEncryption struct {
|
|
// Support describes the level of hardware support available.
|
|
Support StorageEncryptionSupport `json:"support"`
|
|
|
|
// StorageSafety can have values of asserts.StorageSafety
|
|
StorageSafety string `json:"storage-safety,omitempty"`
|
|
|
|
// Type has values of secboot.EncryptionType: "", "cryptsetup",
|
|
// "cryptsetup-with-inline-crypto-engine"
|
|
Type string `json:"encryption-type,omitempty"`
|
|
|
|
// UnavailableReason describes why the encryption is not
|
|
// available in a human readable form. Depending on if
|
|
// encryption is required or not this should be presented to
|
|
// the user as either an error or as information.
|
|
UnavailableReason string `json:"unavailable-reason,omitempty"`
|
|
}
|
|
|
|
type SystemDetails struct {
|
|
// First part is designed to look like `client.System` - the
|
|
// only difference is how the model is represented
|
|
Current bool `json:"current,omitempty"`
|
|
Label string `json:"label,omitempty"`
|
|
Model map[string]interface{} `json:"model,omitempty"`
|
|
Brand snap.StoreAccount `json:"brand,omitempty"`
|
|
Actions []SystemAction `json:"actions,omitempty"`
|
|
|
|
// Volumes contains the volumes defined from the gadget snap
|
|
Volumes map[string]*gadget.Volume `json:"volumes,omitempty"`
|
|
|
|
StorageEncryption *StorageEncryption `json:"storage-encryption,omitempty"`
|
|
}
|
|
|
|
func (client *Client) SystemDetails(systemLabel string) (*SystemDetails, error) {
|
|
var rsp SystemDetails
|
|
|
|
if _, err := client.doSync("GET", "/v2/systems/"+systemLabel, nil, nil, nil, &rsp); err != nil {
|
|
return nil, xerrors.Errorf("cannot get details for system %q: %v", systemLabel, err)
|
|
}
|
|
gadget.SetEnclosingVolumeInStructs(rsp.Volumes)
|
|
return &rsp, nil
|
|
}
|
|
|
|
type InstallStep string
|
|
|
|
const (
|
|
// Creates a change to setup encryption for the partitions
|
|
// with system-{data,save} roles. The successful change has a
|
|
// created device mapper devices ready to use.
|
|
InstallStepSetupStorageEncryption InstallStep = "setup-storage-encryption"
|
|
|
|
// Creates a change to finish the installation. The change
|
|
// ensures all volume structure content is written to disk, it
|
|
// sets up boot, kernel etc and when finished the installed
|
|
// system is ready for reboot.
|
|
InstallStepFinish InstallStep = "finish"
|
|
)
|
|
|
|
type InstallSystemOptions struct {
|
|
// Step is the install step, either "setup-storage-encryption"
|
|
// or "finish".
|
|
Step InstallStep `json:"step,omitempty"`
|
|
|
|
// OnVolumes is the volume description of the volumes that the
|
|
// given step should operate on.
|
|
OnVolumes map[string]*gadget.Volume `json:"on-volumes,omitempty"`
|
|
}
|
|
|
|
// InstallSystem will perform the given install step for the given volumes
|
|
func (client *Client) InstallSystem(systemLabel string, opts *InstallSystemOptions) (changeID string, err error) {
|
|
if systemLabel == "" {
|
|
return "", fmt.Errorf("cannot install with an empty system label")
|
|
}
|
|
|
|
// verification is done by the backend
|
|
req := struct {
|
|
Action string `json:"action"`
|
|
*InstallSystemOptions
|
|
}{
|
|
Action: "install",
|
|
InstallSystemOptions: opts,
|
|
}
|
|
|
|
var body bytes.Buffer
|
|
if err := json.NewEncoder(&body).Encode(&req); err != nil {
|
|
return "", err
|
|
}
|
|
chgID, err := client.doAsync("POST", "/v2/systems/"+systemLabel, nil, nil, &body)
|
|
if err != nil {
|
|
return "", xerrors.Errorf("cannot request system install for %q: %v", systemLabel, err)
|
|
}
|
|
return chgID, nil
|
|
}
|
|
|
|
// CreateSystemOptions contains the options for creating a new recovery system.
|
|
type CreateSystemOptions struct {
|
|
// Label is the label of the new system.
|
|
Label string `json:"label,omitempty"`
|
|
// ValidationSets is a list of validation sets that snaps in the newly
|
|
// created system should be validated against.
|
|
ValidationSets []string `json:"validation-sets,omitempty"`
|
|
// TestSystem is true if the system should be tested by rebooting into the
|
|
// new system.
|
|
TestSystem bool `json:"test-system,omitempty"`
|
|
// MarkDefault is true if the system should be marked as the default
|
|
// recovery system.
|
|
MarkDefault bool `json:"mark-default,omitempty"`
|
|
// Offline is true if the system should be created without reaching out to
|
|
// the store. In the JSON variant of the API, only pre-installed
|
|
// snaps/assertions will be considered.
|
|
Offline bool `json:"offline,omitempty"`
|
|
}
|