File: systems.go

package info (click to toggle)
snapd 2.71-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 79,536 kB
  • sloc: ansic: 16,114; sh: 16,105; python: 9,941; makefile: 1,890; exp: 190; awk: 40; xml: 22
file content (361 lines) | stat: -rw-r--r-- 12,655 bytes parent folder | download
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"`
}