1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
|
// -*- 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/gadget/device"
"github.com/snapcore/snapd/secboot"
"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 (
// forcefully 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 StorageEncryptionFeature string
const (
// Indicates that passphrase authentication is available.
StorageEncryptionFeaturePassphraseAuth StorageEncryptionFeature = "passphrase-auth"
// Indicates that PIN authentication is available.
StorageEncryptionFeaturePINAuth StorageEncryptionFeature = "pin-auth"
)
type StorageEncryption struct {
// Support describes the level of hardware support available.
Support StorageEncryptionSupport `json:"support"`
// Features is a list of available encryption features.
Features []StorageEncryptionFeature `json:"features"`
// StorageSafety can have values of asserts.StorageSafety
StorageSafety string `json:"storage-safety,omitempty"`
// Type has values of device.EncryptionType.
Type device.EncryptionType `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"`
// AvailabilityCheckErrors reports errors detected during preinstall check.
AvailabilityCheckErrors []secboot.PreinstallErrorDetails `json:"availability-check-errors,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]any `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"`
// AvailableOptional contains the optional snaps and components that are
// available in this system.
AvailableOptional AvailableForInstall `json:"available-optional"`
}
// AvailableForInstall contains information about snaps and components that are
// optional in the system's model, but are available for installation.
type AvailableForInstall struct {
// Snaps contains the names of optional snaps that are available for installation.
Snaps []string `json:"snaps,omitempty"`
// Components contains a mapping of snap names to lists of the names of
// optional components that are available for installation.
Components map[string][]string `json:"components,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"
// Generates a recovery key to be used in the "finish" step.
// InstallStepSetupStorageEncryption must be called before calling
// this step.
InstallStepGenerateRecoveryKey InstallStep = "generate-recovery-key"
// 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"`
// OptionalInstall contains the optional snaps and components that should be
// installed on the system. Omitting this field will result in all optional
// snaps and components being installed. To install none of the optional
// snaps and components, provide an empty OptionalInstallRequest with the
// All field set to false.
OptionalInstall *OptionalInstallRequest `json:"optional-install,omitempty"`
// VolumesAuth contains options for volumes authentication (e.g. passphrase
// authentication). If VolumesAuth is nil, the default is to have no
// authentication.
VolumesAuth *device.VolumesAuthOptions `json:"volumes-auth,omitempty"`
}
type OptionalInstallRequest struct {
AvailableForInstall
// All is true if all optional snaps and components should be installed. It
// is invalid to set both All and the individual fields in AvailableForInstall.
All bool `json:"all,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
}
// GeneratePreInstallRecoveryKey generates a recovery key to be enrolled in
// the finish step `InstallStepFinish`.
//
// Note: `InstallStepSetupStorageEncryption` must be called before recovery
// key generation.
func (client *Client) GeneratePreInstallRecoveryKey(systemLabel string) (recoveryKey string, err error) {
if systemLabel == "" {
return "", fmt.Errorf("cannot generate recovery key with an empty system label")
}
req := struct {
Action string `json:"action"`
*InstallSystemOptions
}{
Action: "install",
InstallSystemOptions: &InstallSystemOptions{
Step: "generate-recovery-key",
},
}
var body bytes.Buffer
if err := json.NewEncoder(&body).Encode(&req); err != nil {
return "", err
}
var rsp struct {
RecoveryKey string `json:"recovery-key"`
}
if _, err := client.doSync("POST", "/v2/systems/"+systemLabel, nil, nil, &body, &rsp); err != nil {
return "", xerrors.Errorf("cannot generate recovery key for system %q: %v", systemLabel, err)
}
return rsp.RecoveryKey, 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"`
}
// QualityCheckOptions contains the passphrase or PIN whose quality should be checked.
type QualityCheckOptions struct {
Passphrase string `json:"passphrase,omitempty"`
PIN string `json:"pin,omitempty"`
}
|