// -*- Mode: Go; indent-tabs-mode: t -*- /* * Copyright (C) 2020 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 . * */ package daemon import ( "encoding/json" "errors" "mime" "mime/multipart" "net/http" "os" "strconv" "strings" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/asserts/snapasserts" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/gadget" "github.com/snapcore/snapd/overlord/assertstate" "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/devicestate" "github.com/snapcore/snapd/overlord/install" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" ) var systemsCmd = &Command{ Path: "/v2/systems", GET: getAllSystems, ReadAccess: authenticatedAccess{}, // this is awkward, we want the postSystemsAction function to be used // when the label is empty too, but the router will not handle the request // for /v2/systems with the systemsActionCmd and instead handles it through // this command, so we need to set the POST for this command to essentially // forward to that one POST: postSystemsAction, WriteAccess: rootAccess{}, } var systemsActionCmd = &Command{ Path: "/v2/systems/{label}", GET: getSystemDetails, ReadAccess: rootAccess{}, POST: postSystemsAction, WriteAccess: rootAccess{}, } type systemsResponse struct { Systems []client.System `json:"systems,omitempty"` } func getAllSystems(c *Command, r *http.Request, user *auth.UserState) Response { var rsp systemsResponse seedSystems, err := c.d.overlord.DeviceManager().Systems() if err != nil { if err == devicestate.ErrNoSystems { // no systems available return SyncResponse(&rsp) } return InternalError(err.Error()) } rsp.Systems = make([]client.System, 0, len(seedSystems)) for _, ss := range seedSystems { // untangle the model actions := make([]client.SystemAction, 0, len(ss.Actions)) for _, sa := range ss.Actions { actions = append(actions, client.SystemAction{ Title: sa.Title, Mode: sa.Mode, }) } rsp.Systems = append(rsp.Systems, client.System{ Current: ss.Current, DefaultRecoverySystem: ss.DefaultRecoverySystem, Label: ss.Label, Model: client.SystemModelData{ Model: ss.Model.Model(), BrandID: ss.Model.BrandID(), DisplayName: ss.Model.DisplayName(), }, Brand: snap.StoreAccount{ ID: ss.Brand.AccountID(), Username: ss.Brand.Username(), DisplayName: ss.Brand.DisplayName(), Validation: ss.Brand.Validation(), }, Actions: actions, }) } return SyncResponse(&rsp) } // wrapped for unit tests var deviceManagerSystemAndGadgetAndEncryptionInfo = func(dm *devicestate.DeviceManager, systemLabel string) (*devicestate.System, *gadget.Info, *install.EncryptionSupportInfo, error) { return dm.SystemAndGadgetAndEncryptionInfo(systemLabel) } func storageEncryption(encInfo *install.EncryptionSupportInfo) *client.StorageEncryption { if encInfo.Disabled { return &client.StorageEncryption{ Support: client.StorageEncryptionSupportDisabled, } } storageEnc := &client.StorageEncryption{ StorageSafety: string(encInfo.StorageSafety), Type: string(encInfo.Type), } required := (encInfo.StorageSafety == asserts.StorageSafetyEncrypted) switch { case encInfo.Available: storageEnc.Support = client.StorageEncryptionSupportAvailable case !encInfo.Available && required: storageEnc.Support = client.StorageEncryptionSupportDefective storageEnc.UnavailableReason = encInfo.UnavailableErr.Error() case !encInfo.Available && !required: storageEnc.Support = client.StorageEncryptionSupportUnavailable storageEnc.UnavailableReason = encInfo.UnavailableWarning } return storageEnc } var ( devicestateInstallFinish = devicestate.InstallFinish devicestateInstallSetupStorageEncryption = devicestate.InstallSetupStorageEncryption devicestateCreateRecoverySystem = devicestate.CreateRecoverySystem devicestateRemoveRecoverySystem = devicestate.RemoveRecoverySystem ) func getSystemDetails(c *Command, r *http.Request, user *auth.UserState) Response { wantedSystemLabel := muxVars(r)["label"] deviceMgr := c.d.overlord.DeviceManager() sys, gadgetInfo, encryptionInfo, err := deviceManagerSystemAndGadgetAndEncryptionInfo(deviceMgr, wantedSystemLabel) if err != nil { return InternalError(err.Error()) } rsp := client.SystemDetails{ Current: sys.Current, Label: sys.Label, Brand: snap.StoreAccount{ ID: sys.Brand.AccountID(), Username: sys.Brand.Username(), DisplayName: sys.Brand.DisplayName(), Validation: sys.Brand.Validation(), }, // no body: we expect models to have empty bodies Model: sys.Model.Headers(), Volumes: gadgetInfo.Volumes, StorageEncryption: storageEncryption(encryptionInfo), } for _, sa := range sys.Actions { rsp.Actions = append(rsp.Actions, client.SystemAction{ Title: sa.Title, Mode: sa.Mode, }) } return SyncResponse(rsp) } type systemActionRequest struct { Action string `json:"action"` client.SystemAction client.InstallSystemOptions client.CreateSystemOptions } func postSystemsAction(c *Command, r *http.Request, user *auth.UserState) Response { contentType := r.Header.Get("Content-Type") mediaType, params, err := mime.ParseMediaType(contentType) if err != nil { mediaType = "application/json" } switch mediaType { case "application/json": return postSystemsActionJSON(c, r) case "multipart/form-data": return postSystemsActionForm(c, r, params) default: return BadRequest("unexpected media type %q", mediaType) } } func postSystemsActionForm(c *Command, r *http.Request, contentTypeParams map[string]string) (res Response) { boundary := contentTypeParams["boundary"] mpReader := multipart.NewReader(r.Body, boundary) form, errRsp := readForm(mpReader) if errRsp != nil { return errRsp } action := form.Values["action"] if len(action) != 1 { return BadRequest("expected exactly one action in form") } defer func() { // remove all files associated with the form if we're returning an error if _, ok := res.(*apiError); ok { form.RemoveAllExcept(nil) } }() switch action[0] { case "create": return postSystemActionCreateOffline(c, form) default: return BadRequest("%s action is not supported for content type multipart/form-data", action[0]) } } func postSystemsActionJSON(c *Command, r *http.Request) Response { var req systemActionRequest systemLabel := muxVars(r)["label"] decoder := json.NewDecoder(r.Body) if err := decoder.Decode(&req); err != nil { return BadRequest("cannot decode request body into system action: %v", err) } if decoder.More() { return BadRequest("extra content found in request body") } switch req.Action { case "do": return postSystemActionDo(c, systemLabel, &req) case "reboot": return postSystemActionReboot(c, systemLabel, &req) case "install": return postSystemActionInstall(c, systemLabel, &req) case "create": if systemLabel != "" { return BadRequest("label should not be provided in route when creating a system") } return postSystemActionCreate(c, &req) case "remove": return postSystemActionRemove(c, systemLabel) default: return BadRequest("unsupported action %q", req.Action) } } // XXX: should deviceManager return more sensible errors here? e.g.: // UnsupportedActionError{systemLabel, mode}, SystemDoesNotExistError{systemLabel} func handleSystemActionErr(err error, systemLabel string) Response { if os.IsNotExist(err) { return NotFound("requested seed system %q does not exist", systemLabel) } if err == devicestate.ErrUnsupportedAction { return BadRequest("requested action is not supported by system %q", systemLabel) } return InternalError(err.Error()) } // wrapped for unit tests var deviceManagerReboot = func(dm *devicestate.DeviceManager, systemLabel, mode string) error { return dm.Reboot(systemLabel, mode) } func postSystemActionReboot(c *Command, systemLabel string, req *systemActionRequest) Response { dm := c.d.overlord.DeviceManager() if err := deviceManagerReboot(dm, systemLabel, req.Mode); err != nil { return handleSystemActionErr(err, systemLabel) } return SyncResponse(nil) } func postSystemActionDo(c *Command, systemLabel string, req *systemActionRequest) Response { if systemLabel == "" { return BadRequest("system action requires the system label to be provided") } if req.Mode == "" { return BadRequest("system action requires the mode to be provided") } sa := devicestate.SystemAction{ Title: req.Title, Mode: req.Mode, } if err := c.d.overlord.DeviceManager().RequestSystemAction(systemLabel, sa); err != nil { return handleSystemActionErr(err, systemLabel) } return SyncResponse(nil) } func postSystemActionInstall(c *Command, systemLabel string, req *systemActionRequest) Response { st := c.d.overlord.State() st.Lock() defer st.Unlock() switch req.Step { case client.InstallStepSetupStorageEncryption: chg, err := devicestateInstallSetupStorageEncryption(st, systemLabel, req.OnVolumes) if err != nil { return BadRequest("cannot setup storage encryption for install from %q: %v", systemLabel, err) } ensureStateSoon(st) return AsyncResponse(nil, chg.ID()) case client.InstallStepFinish: chg, err := devicestateInstallFinish(st, systemLabel, req.OnVolumes) if err != nil { return BadRequest("cannot finish install for %q: %v", systemLabel, err) } ensureStateSoon(st) return AsyncResponse(nil, chg.ID()) default: return BadRequest("unsupported install step %q", req.Step) } } func assertionsFromValidationSetStrings(validationSets []string) ([]*asserts.AtSequence, error) { sets := make([]*asserts.AtSequence, 0, len(validationSets)) for _, vs := range validationSets { account, name, seq, err := snapasserts.ParseValidationSet(vs) if err != nil { return nil, err } assertion := asserts.AtSequence{ Type: asserts.ValidationSetType, SequenceKey: []string{release.Series, account, name}, Pinned: seq > 0, Sequence: seq, Revision: asserts.RevisionNotKnown, } sets = append(sets, &assertion) } return sets, nil } func readFormValue(form *Form, key string) (string, *apiError) { values := form.Values[key] if len(values) != 1 { return "", BadRequest("expected exactly one %q value in form", key) } return values[0], nil } func readOptionalFormValue(form *Form, key string, defaultValue string) (string, *apiError) { values := form.Values[key] switch len(values) { case 0: return defaultValue, nil case 1: return values[0], nil default: return "", BadRequest("expected at most one %q value in form", key) } } func readOptionalFormBoolean(form *Form, key string, defaultValue bool) (bool, *apiError) { values := form.Values[key] switch len(values) { case 0: return defaultValue, nil case 1: b, err := strconv.ParseBool(values[0]) if err != nil { return false, BadRequest("cannot parse %q value as boolean: %s", key, values[0]) } return b, nil default: return false, BadRequest("expected at most one %q value in form", key) } } func postSystemActionCreateOffline(c *Command, form *Form) Response { label, errRsp := readFormValue(form, "label") if errRsp != nil { return errRsp } testSystem, errRsp := readOptionalFormBoolean(form, "test-system", false) if errRsp != nil { return errRsp } markDefault, errRsp := readOptionalFormBoolean(form, "mark-default", false) if errRsp != nil { return errRsp } vsetsList, errRsp := readOptionalFormValue(form, "validation-sets", "") if errRsp != nil { return errRsp } var splitVSets []string if vsetsList != "" { splitVSets = strings.Split(vsetsList, ",") } // this could be multiple "validation-set" values, but that would make it so // that the field names in the form and JSON APIs are different, since the // JSON API uses "validation-sets" (plural). to keep the APIs consistent, we // use a comma-delimeted list of validation sets strings. sequences, err := assertionsFromValidationSetStrings(splitVSets) if err != nil { return BadRequest("cannot parse validation sets: %v", err) } var snapFiles []*uploadedSnap if len(form.FileRefs["snap"]) > 0 { snaps, errRsp := form.GetSnapFiles() if errRsp != nil { return errRsp } snapFiles = snaps } batch := asserts.NewBatch(nil) for _, a := range form.Values["assertion"] { if _, err := batch.AddStream(strings.NewReader(a)); err != nil { return BadRequest("cannot decode assertion: %v", err) } } st := c.d.overlord.State() st.Lock() defer st.Unlock() if err := assertstate.AddBatch(st, batch, &asserts.CommitOptions{Precheck: true}); err != nil { return BadRequest("error committing assertions: %v", err) } validationSets, err := assertstate.FetchValidationSets(st, sequences, assertstate.FetchValidationSetsOptions{ Offline: true, }, nil) if err != nil { return BadRequest("cannot find validation sets in db: %v", err) } slInfo, apiErr := sideloadSnapsInfo(st, snapFiles, sideloadFlags{}) if apiErr != nil { return apiErr } if len(slInfo.sideInfos) != len(slInfo.tmpPaths) { return InternalError("mismatch between number of snap side infos and temporary paths") } localSnaps := make([]devicestate.LocalSnap, 0, len(slInfo.sideInfos)) for i := range slInfo.sideInfos { localSnaps = append(localSnaps, devicestate.LocalSnap{ SideInfo: slInfo.sideInfos[i], Path: slInfo.tmpPaths[i], }) } chg, err := devicestateCreateRecoverySystem(st, label, devicestate.CreateRecoverySystemOptions{ ValidationSets: validationSets.Sets(), LocalSnaps: localSnaps, TestSystem: testSystem, MarkDefault: markDefault, // using the form-based API implies that this should be an offline operation Offline: true, }) if err != nil { return InternalError("cannot create recovery system %q: %v", label[0], err) } ensureStateSoon(st) return AsyncResponse(nil, chg.ID()) } func postSystemActionCreate(c *Command, req *systemActionRequest) Response { st := c.d.overlord.State() st.Lock() defer st.Unlock() if req.Label == "" { return BadRequest("label must be provided in request body for action %q", req.Action) } sequences, err := assertionsFromValidationSetStrings(req.ValidationSets) if err != nil { return BadRequest("cannot parse validation sets: %v", err) } validationSets, err := assertstate.FetchValidationSets(c.d.state, sequences, assertstate.FetchValidationSetsOptions{ Offline: req.Offline, }, nil) if err != nil { if errors.Is(err, &asserts.NotFoundError{}) { return BadRequest("cannot fetch validation sets: %v", err) } return InternalError("cannot fetch validation sets: %v", err) } chg, err := devicestateCreateRecoverySystem(st, req.Label, devicestate.CreateRecoverySystemOptions{ ValidationSets: validationSets.Sets(), TestSystem: req.TestSystem, MarkDefault: req.MarkDefault, Offline: req.Offline, }) if err != nil { return InternalError("cannot create recovery system %q: %v", req.Label, err) } ensureStateSoon(st) return AsyncResponse(nil, chg.ID()) } func postSystemActionRemove(c *Command, systemLabel string) Response { if systemLabel == "" { return BadRequest("system action requires the system label to be provided") } st := c.d.overlord.State() st.Lock() defer st.Unlock() chg, err := devicestateRemoveRecoverySystem(st, systemLabel) if err != nil { if errors.Is(err, devicestate.ErrNoRecoverySystem) { return NotFound(err.Error()) } return InternalError("cannot remove recovery system %q: %v", systemLabel, err) } ensureStateSoon(st) return AsyncResponse(nil, chg.ID()) }