Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| // -*- Mode: Go; indent-tabs-mode: t -*- | |
| /* | |
| * Copyright (C) 2015-2016 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 daemon | |
| import ( | |
| "encoding/json" | |
| "errors" | |
| "fmt" | |
| "io" | |
| "io/ioutil" | |
| "mime" | |
| "mime/multipart" | |
| "net" | |
| "net/http" | |
| "os" | |
| "os/user" | |
| "path/filepath" | |
| "regexp" | |
| "sort" | |
| "strconv" | |
| "strings" | |
| "time" | |
| "github.com/gorilla/mux" | |
| "github.com/jessevdk/go-flags" | |
| "github.com/snapcore/snapd/asserts" | |
| "github.com/snapcore/snapd/asserts/snapasserts" | |
| "github.com/snapcore/snapd/client" | |
| "github.com/snapcore/snapd/dirs" | |
| "github.com/snapcore/snapd/i18n" | |
| "github.com/snapcore/snapd/interfaces" | |
| "github.com/snapcore/snapd/jsonutil" | |
| "github.com/snapcore/snapd/logger" | |
| "github.com/snapcore/snapd/osutil" | |
| "github.com/snapcore/snapd/overlord/assertstate" | |
| "github.com/snapcore/snapd/overlord/auth" | |
| "github.com/snapcore/snapd/overlord/configstate" | |
| "github.com/snapcore/snapd/overlord/configstate/config" | |
| "github.com/snapcore/snapd/overlord/devicestate" | |
| "github.com/snapcore/snapd/overlord/hookstate/ctlcmd" | |
| "github.com/snapcore/snapd/overlord/ifacestate" | |
| "github.com/snapcore/snapd/overlord/servicestate" | |
| "github.com/snapcore/snapd/overlord/snapstate" | |
| "github.com/snapcore/snapd/overlord/state" | |
| "github.com/snapcore/snapd/progress" | |
| "github.com/snapcore/snapd/release" | |
| "github.com/snapcore/snapd/snap" | |
| "github.com/snapcore/snapd/store" | |
| "github.com/snapcore/snapd/strutil" | |
| "github.com/snapcore/snapd/systemd" | |
| ) | |
| var api = []*Command{ | |
| rootCmd, | |
| sysInfoCmd, | |
| loginCmd, | |
| logoutCmd, | |
| appIconCmd, | |
| findCmd, | |
| snapsCmd, | |
| snapCmd, | |
| snapConfCmd, | |
| interfacesCmd, | |
| assertsCmd, | |
| assertsFindManyCmd, | |
| stateChangeCmd, | |
| stateChangesCmd, | |
| createUserCmd, | |
| buyCmd, | |
| readyToBuyCmd, | |
| snapctlCmd, | |
| usersCmd, | |
| sectionsCmd, | |
| aliasesCmd, | |
| appsCmd, | |
| logsCmd, | |
| debugCmd, | |
| } | |
| var ( | |
| rootCmd = &Command{ | |
| Path: "/", | |
| GuestOK: true, | |
| GET: tbd, | |
| } | |
| sysInfoCmd = &Command{ | |
| Path: "/v2/system-info", | |
| GuestOK: true, | |
| GET: sysInfo, | |
| } | |
| loginCmd = &Command{ | |
| Path: "/v2/login", | |
| POST: loginUser, | |
| PolkitOK: "io.snapcraft.snapd.login", | |
| } | |
| logoutCmd = &Command{ | |
| Path: "/v2/logout", | |
| POST: logoutUser, | |
| UserOK: true, | |
| } | |
| appIconCmd = &Command{ | |
| Path: "/v2/icons/{name}/icon", | |
| UserOK: true, | |
| GET: appIconGet, | |
| } | |
| findCmd = &Command{ | |
| Path: "/v2/find", | |
| UserOK: true, | |
| GET: searchStore, | |
| } | |
| snapsCmd = &Command{ | |
| Path: "/v2/snaps", | |
| UserOK: true, | |
| PolkitOK: "io.snapcraft.snapd.manage", | |
| GET: getSnapsInfo, | |
| POST: postSnaps, | |
| } | |
| snapCmd = &Command{ | |
| Path: "/v2/snaps/{name}", | |
| UserOK: true, | |
| PolkitOK: "io.snapcraft.snapd.manage", | |
| GET: getSnapInfo, | |
| POST: postSnap, | |
| } | |
| appsCmd = &Command{ | |
| Path: "/v2/apps", | |
| UserOK: true, | |
| GET: getAppsInfo, | |
| POST: postApps, | |
| } | |
| logsCmd = &Command{ | |
| Path: "/v2/logs", | |
| PolkitOK: "io.snapcraft.snapd.manage", | |
| GET: getLogs, | |
| } | |
| snapConfCmd = &Command{ | |
| Path: "/v2/snaps/{name}/conf", | |
| GET: getSnapConf, | |
| PUT: setSnapConf, | |
| } | |
| interfacesCmd = &Command{ | |
| Path: "/v2/interfaces", | |
| UserOK: true, | |
| PolkitOK: "io.snapcraft.snapd.manage-interfaces", | |
| GET: interfacesConnectionsMultiplexer, | |
| POST: changeInterfaces, | |
| } | |
| // TODO: allow to post assertions for UserOK? they are verified anyway | |
| assertsCmd = &Command{ | |
| Path: "/v2/assertions", | |
| UserOK: true, | |
| GET: getAssertTypeNames, | |
| POST: doAssert, | |
| } | |
| assertsFindManyCmd = &Command{ | |
| Path: "/v2/assertions/{assertType}", | |
| UserOK: true, | |
| GET: assertsFindMany, | |
| } | |
| stateChangeCmd = &Command{ | |
| Path: "/v2/changes/{id}", | |
| UserOK: true, | |
| PolkitOK: "io.snapcraft.snapd.manage", | |
| GET: getChange, | |
| POST: abortChange, | |
| } | |
| stateChangesCmd = &Command{ | |
| Path: "/v2/changes", | |
| UserOK: true, | |
| GET: getChanges, | |
| } | |
| debugCmd = &Command{ | |
| Path: "/v2/debug", | |
| POST: postDebug, | |
| } | |
| createUserCmd = &Command{ | |
| Path: "/v2/create-user", | |
| UserOK: false, | |
| POST: postCreateUser, | |
| } | |
| buyCmd = &Command{ | |
| Path: "/v2/buy", | |
| UserOK: false, | |
| POST: postBuy, | |
| } | |
| readyToBuyCmd = &Command{ | |
| Path: "/v2/buy/ready", | |
| UserOK: false, | |
| GET: readyToBuy, | |
| } | |
| snapctlCmd = &Command{ | |
| Path: "/v2/snapctl", | |
| SnapOK: true, | |
| POST: runSnapctl, | |
| } | |
| usersCmd = &Command{ | |
| Path: "/v2/users", | |
| UserOK: false, | |
| GET: getUsers, | |
| } | |
| sectionsCmd = &Command{ | |
| Path: "/v2/sections", | |
| UserOK: true, | |
| GET: getSections, | |
| } | |
| aliasesCmd = &Command{ | |
| Path: "/v2/aliases", | |
| UserOK: true, | |
| GET: getAliases, | |
| POST: changeAliases, | |
| } | |
| ) | |
| func tbd(c *Command, r *http.Request, user *auth.UserState) Response { | |
| return SyncResponse([]string{"TBD"}, nil) | |
| } | |
| func formatRefreshTime(t time.Time) string { | |
| if t.IsZero() { | |
| return "" | |
| } | |
| return fmt.Sprintf("%s", t.Truncate(time.Minute).Format(time.RFC3339)) | |
| } | |
| func sysInfo(c *Command, r *http.Request, user *auth.UserState) Response { | |
| st := c.d.overlord.State() | |
| snapMgr := c.d.overlord.SnapManager() | |
| st.Lock() | |
| nextRefresh := snapMgr.NextRefresh() | |
| lastRefresh, _ := snapMgr.LastRefresh() | |
| refreshScheduleStr, err := snapMgr.RefreshSchedule() | |
| if err != nil { | |
| return InternalError("cannot get refresh schedule: %s", err) | |
| } | |
| users, err := auth.Users(st) | |
| st.Unlock() | |
| if err != nil && err != state.ErrNoState { | |
| return InternalError("cannot get user auth data: %s", err) | |
| } | |
| m := map[string]interface{}{ | |
| "series": release.Series, | |
| "version": c.d.Version, | |
| "os-release": release.ReleaseInfo, | |
| "on-classic": release.OnClassic, | |
| "managed": len(users) > 0, | |
| "kernel-version": release.KernelVersion(), | |
| "locations": map[string]interface{}{ | |
| "snap-mount-dir": dirs.SnapMountDir, | |
| "snap-bin-dir": dirs.SnapBinariesDir, | |
| }, | |
| "refresh": client.RefreshInfo{ | |
| Schedule: refreshScheduleStr, | |
| Last: formatRefreshTime(lastRefresh), | |
| Next: formatRefreshTime(nextRefresh), | |
| }, | |
| } | |
| // NOTE: Right now we don't have a good way to differentiate if we | |
| // only have partial confinement (ala AppArmor disabled and Seccomp | |
| // enabled) or no confinement at all. Once we have a better system | |
| // in place how we can dynamically retrieve these information from | |
| // snapd we will use this here. | |
| if release.ReleaseInfo.ForceDevMode() { | |
| m["confinement"] = "partial" | |
| } else { | |
| m["confinement"] = "strict" | |
| } | |
| return SyncResponse(m, nil) | |
| } | |
| // userResponseData contains the data releated to user creation/login/query | |
| type userResponseData struct { | |
| ID int `json:"id,omitempty"` | |
| Username string `json:"username,omitempty"` | |
| Email string `json:"email,omitempty"` | |
| SSHKeys []string `json:"ssh-keys,omitempty"` | |
| Macaroon string `json:"macaroon,omitempty"` | |
| Discharges []string `json:"discharges,omitempty"` | |
| } | |
| var isEmailish = regexp.MustCompile(`.@.*\..`).MatchString | |
| func loginUser(c *Command, r *http.Request, user *auth.UserState) Response { | |
| var loginData struct { | |
| Username string `json:"username"` | |
| Email string `json:"email"` | |
| Password string `json:"password"` | |
| Otp string `json:"otp"` | |
| } | |
| decoder := json.NewDecoder(r.Body) | |
| if err := decoder.Decode(&loginData); err != nil { | |
| return BadRequest("cannot decode login data from request body: %v", err) | |
| } | |
| if loginData.Email == "" && isEmailish(loginData.Username) { | |
| // for backwards compatibility, if no email is provided assume username is the email | |
| loginData.Email = loginData.Username | |
| loginData.Username = "" | |
| } | |
| if loginData.Email == "" && user != nil && user.Email != "" { | |
| loginData.Email = user.Email | |
| } | |
| // the "username" needs to look a lot like an email address | |
| if !isEmailish(loginData.Email) { | |
| return SyncResponse(&resp{ | |
| Type: ResponseTypeError, | |
| Result: &errorResult{ | |
| Message: "please use a valid email address.", | |
| Kind: errorKindInvalidAuthData, | |
| Value: map[string][]string{"email": {"invalid"}}, | |
| }, | |
| Status: 400, | |
| }, nil) | |
| } | |
| macaroon, discharge, err := store.LoginUser(loginData.Email, loginData.Password, loginData.Otp) | |
| switch err { | |
| case store.ErrAuthenticationNeeds2fa: | |
| return SyncResponse(&resp{ | |
| Type: ResponseTypeError, | |
| Result: &errorResult{ | |
| Kind: errorKindTwoFactorRequired, | |
| Message: err.Error(), | |
| }, | |
| Status: 401, | |
| }, nil) | |
| case store.Err2faFailed: | |
| return SyncResponse(&resp{ | |
| Type: ResponseTypeError, | |
| Result: &errorResult{ | |
| Kind: errorKindTwoFactorFailed, | |
| Message: err.Error(), | |
| }, | |
| Status: 401, | |
| }, nil) | |
| default: | |
| switch err := err.(type) { | |
| case store.InvalidAuthDataError: | |
| return SyncResponse(&resp{ | |
| Type: ResponseTypeError, | |
| Result: &errorResult{ | |
| Message: err.Error(), | |
| Kind: errorKindInvalidAuthData, | |
| Value: err, | |
| }, | |
| Status: 400, | |
| }, nil) | |
| case store.PasswordPolicyError: | |
| return SyncResponse(&resp{ | |
| Type: ResponseTypeError, | |
| Result: &errorResult{ | |
| Message: err.Error(), | |
| Kind: errorKindPasswordPolicy, | |
| Value: err, | |
| }, | |
| Status: 401, | |
| }, nil) | |
| } | |
| return Unauthorized(err.Error()) | |
| case nil: | |
| // continue | |
| } | |
| overlord := c.d.overlord | |
| state := overlord.State() | |
| state.Lock() | |
| if user != nil { | |
| // local user logged-in, set its store macaroons | |
| user.StoreMacaroon = macaroon | |
| user.StoreDischarges = []string{discharge} | |
| err = auth.UpdateUser(state, user) | |
| } else { | |
| user, err = auth.NewUser(state, loginData.Username, loginData.Email, macaroon, []string{discharge}) | |
| } | |
| state.Unlock() | |
| if err != nil { | |
| return InternalError("cannot persist authentication details: %v", err) | |
| } | |
| result := userResponseData{ | |
| ID: user.ID, | |
| Username: user.Username, | |
| Email: user.Email, | |
| Macaroon: user.Macaroon, | |
| Discharges: user.Discharges, | |
| } | |
| return SyncResponse(result, nil) | |
| } | |
| func logoutUser(c *Command, r *http.Request, user *auth.UserState) Response { | |
| state := c.d.overlord.State() | |
| state.Lock() | |
| defer state.Unlock() | |
| if user == nil { | |
| return BadRequest("not logged in") | |
| } | |
| err := auth.RemoveUser(state, user.ID) | |
| if err != nil { | |
| return InternalError(err.Error()) | |
| } | |
| return SyncResponse(nil, nil) | |
| } | |
| // UserFromRequest extracts user information from request and return the respective user in state, if valid | |
| // It requires the state to be locked | |
| func UserFromRequest(st *state.State, req *http.Request) (*auth.UserState, error) { | |
| // extract macaroons data from request | |
| header := req.Header.Get("Authorization") | |
| if header == "" { | |
| return nil, auth.ErrInvalidAuth | |
| } | |
| authorizationData := strings.SplitN(header, " ", 2) | |
| if len(authorizationData) != 2 || authorizationData[0] != "Macaroon" { | |
| return nil, fmt.Errorf("authorization header misses Macaroon prefix") | |
| } | |
| var macaroon string | |
| var discharges []string | |
| for _, field := range splitQS(authorizationData[1]) { | |
| if strings.HasPrefix(field, `root="`) { | |
| macaroon = strings.TrimSuffix(field[6:], `"`) | |
| } | |
| if strings.HasPrefix(field, `discharge="`) { | |
| discharges = append(discharges, strings.TrimSuffix(field[11:], `"`)) | |
| } | |
| } | |
| if macaroon == "" { | |
| return nil, fmt.Errorf("invalid authorization header") | |
| } | |
| user, err := auth.CheckMacaroon(st, macaroon, discharges) | |
| return user, err | |
| } | |
| var muxVars = mux.Vars | |
| func getSnapInfo(c *Command, r *http.Request, user *auth.UserState) Response { | |
| vars := muxVars(r) | |
| name := vars["name"] | |
| about, err := localSnapInfo(c.d.overlord.State(), name) | |
| if err != nil { | |
| if err == errNoSnap { | |
| return SnapNotFound(name, err) | |
| } | |
| return InternalError("%v", err) | |
| } | |
| route := c.d.router.Get(c.Path) | |
| if route == nil { | |
| return InternalError("cannot find route for %q snap", name) | |
| } | |
| url, err := route.URL("name", name) | |
| if err != nil { | |
| return InternalError("cannot build URL for %q snap: %v", name, err) | |
| } | |
| result := webify(mapLocal(about), url.String()) | |
| return SyncResponse(result, nil) | |
| } | |
| func webify(result *client.Snap, resource string) *client.Snap { | |
| if result.Icon == "" || strings.HasPrefix(result.Icon, "http") { | |
| return result | |
| } | |
| result.Icon = "" | |
| route := appIconCmd.d.router.Get(appIconCmd.Path) | |
| if route != nil { | |
| url, err := route.URL("name", result.Name) | |
| if err == nil { | |
| result.Icon = url.String() | |
| } | |
| } | |
| return result | |
| } | |
| func getStore(c *Command) snapstate.StoreService { | |
| st := c.d.overlord.State() | |
| st.Lock() | |
| defer st.Unlock() | |
| return snapstate.Store(st) | |
| } | |
| func getSections(c *Command, r *http.Request, user *auth.UserState) Response { | |
| route := c.d.router.Get(snapCmd.Path) | |
| if route == nil { | |
| return InternalError("cannot find route for snaps") | |
| } | |
| theStore := getStore(c) | |
| sections, err := theStore.Sections(user) | |
| switch err { | |
| case nil: | |
| // pass | |
| case store.ErrBadQuery: | |
| return SyncResponse(&resp{ | |
| Type: ResponseTypeError, | |
| Result: &errorResult{Message: err.Error(), Kind: errorKindBadQuery}, | |
| Status: 400, | |
| }, nil) | |
| case store.ErrUnauthenticated, store.ErrInvalidCredentials: | |
| return Unauthorized("%v", err) | |
| default: | |
| return InternalError("%v", err) | |
| } | |
| return SyncResponse(sections, &Meta{}) | |
| } | |
| func searchStore(c *Command, r *http.Request, user *auth.UserState) Response { | |
| route := c.d.router.Get(snapCmd.Path) | |
| if route == nil { | |
| return InternalError("cannot find route for snaps") | |
| } | |
| query := r.URL.Query() | |
| q := query.Get("q") | |
| section := query.Get("section") | |
| name := query.Get("name") | |
| private := false | |
| prefix := false | |
| if name != "" { | |
| if q != "" { | |
| return BadRequest("cannot use 'q' and 'name' together") | |
| } | |
| if name[len(name)-1] != '*' { | |
| return findOne(c, r, user, name) | |
| } | |
| prefix = true | |
| q = name[:len(name)-1] | |
| } | |
| if sel := query.Get("select"); sel != "" { | |
| switch sel { | |
| case "refresh": | |
| if prefix { | |
| return BadRequest("cannot use 'name' with 'select=refresh'") | |
| } | |
| if q != "" { | |
| return BadRequest("cannot use 'q' with 'select=refresh'") | |
| } | |
| return storeUpdates(c, r, user) | |
| case "private": | |
| private = true | |
| } | |
| } | |
| theStore := getStore(c) | |
| found, err := theStore.Find(&store.Search{ | |
| Query: q, | |
| Section: section, | |
| Private: private, | |
| Prefix: prefix, | |
| }, user) | |
| if e, ok := err.(net.Error); ok && e.Timeout() { | |
| return SyncResponse(&resp{ | |
| Type: ResponseTypeError, | |
| Result: &errorResult{Message: err.Error(), Kind: errorKindNetworkTimeout}, | |
| Status: 400, | |
| }, nil) | |
| } | |
| switch err { | |
| case nil: | |
| // pass | |
| case store.ErrBadQuery: | |
| return SyncResponse(&resp{ | |
| Type: ResponseTypeError, | |
| Result: &errorResult{Message: err.Error(), Kind: errorKindBadQuery}, | |
| Status: 400, | |
| }, nil) | |
| case store.ErrUnauthenticated, store.ErrInvalidCredentials: | |
| return Unauthorized(err.Error()) | |
| default: | |
| return InternalError("%v", err) | |
| } | |
| meta := &Meta{ | |
| SuggestedCurrency: theStore.SuggestedCurrency(), | |
| Sources: []string{"store"}, | |
| } | |
| return sendStorePackages(route, meta, found) | |
| } | |
| func findOne(c *Command, r *http.Request, user *auth.UserState, name string) Response { | |
| if err := snap.ValidateName(name); err != nil { | |
| return BadRequest(err.Error()) | |
| } | |
| theStore := getStore(c) | |
| spec := store.SnapSpec{ | |
| Name: name, | |
| AnyChannel: true, | |
| } | |
| snapInfo, err := theStore.SnapInfo(spec, user) | |
| switch err { | |
| case nil: | |
| // pass | |
| case store.ErrInvalidCredentials: | |
| return Unauthorized("%v", err) | |
| case store.ErrSnapNotFound: | |
| return SnapNotFound(name, err) | |
| default: | |
| return InternalError("%v", err) | |
| } | |
| meta := &Meta{ | |
| SuggestedCurrency: theStore.SuggestedCurrency(), | |
| Sources: []string{"store"}, | |
| } | |
| results := make([]*json.RawMessage, 1) | |
| data, err := json.Marshal(webify(mapRemote(snapInfo), r.URL.String())) | |
| if err != nil { | |
| return InternalError(err.Error()) | |
| } | |
| results[0] = (*json.RawMessage)(&data) | |
| return SyncResponse(results, meta) | |
| } | |
| func shouldSearchStore(r *http.Request) bool { | |
| // we should jump to the old behaviour iff q is given, or if | |
| // sources is given and either empty or contains the word | |
| // 'store'. Otherwise, local results only. | |
| query := r.URL.Query() | |
| if _, ok := query["q"]; ok { | |
| logger.Debugf("use of obsolete \"q\" parameter: %q", r.URL) | |
| return true | |
| } | |
| if src, ok := query["sources"]; ok { | |
| logger.Debugf("use of obsolete \"sources\" parameter: %q", r.URL) | |
| if len(src) == 0 || strings.Contains(src[0], "store") { | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| func storeUpdates(c *Command, r *http.Request, user *auth.UserState) Response { | |
| route := c.d.router.Get(snapCmd.Path) | |
| if route == nil { | |
| return InternalError("cannot find route for snaps") | |
| } | |
| state := c.d.overlord.State() | |
| state.Lock() | |
| updates, err := snapstateRefreshCandidates(state, user) | |
| state.Unlock() | |
| if err != nil { | |
| return InternalError("cannot list updates: %v", err) | |
| } | |
| return sendStorePackages(route, nil, updates) | |
| } | |
| func sendStorePackages(route *mux.Route, meta *Meta, found []*snap.Info) Response { | |
| results := make([]*json.RawMessage, 0, len(found)) | |
| for _, x := range found { | |
| url, err := route.URL("name", x.Name()) | |
| if err != nil { | |
| logger.Noticef("Cannot build URL for snap %q revision %s: %v", x.Name(), x.Revision, err) | |
| continue | |
| } | |
| data, err := json.Marshal(webify(mapRemote(x), url.String())) | |
| if err != nil { | |
| return InternalError("%v", err) | |
| } | |
| raw := json.RawMessage(data) | |
| results = append(results, &raw) | |
| } | |
| return SyncResponse(results, meta) | |
| } | |
| // plural! | |
| func getSnapsInfo(c *Command, r *http.Request, user *auth.UserState) Response { | |
| if shouldSearchStore(r) { | |
| logger.Noticef("Jumping to \"find\" to better support legacy request %q", r.URL) | |
| return searchStore(c, r, user) | |
| } | |
| route := c.d.router.Get(snapCmd.Path) | |
| if route == nil { | |
| return InternalError("cannot find route for snaps") | |
| } | |
| query := r.URL.Query() | |
| var all bool | |
| sel := query.Get("select") | |
| switch sel { | |
| case "all": | |
| all = true | |
| case "enabled", "": | |
| all = false | |
| default: | |
| return BadRequest("invalid select parameter: %q", sel) | |
| } | |
| var wanted map[string]bool | |
| if ns := query.Get("snaps"); len(ns) > 0 { | |
| nsl := splitQS(ns) | |
| wanted = make(map[string]bool, len(nsl)) | |
| for _, name := range nsl { | |
| wanted[name] = true | |
| } | |
| } | |
| found, err := allLocalSnapInfos(c.d.overlord.State(), all, wanted) | |
| if err != nil { | |
| return InternalError("cannot list local snaps! %v", err) | |
| } | |
| results := make([]*json.RawMessage, len(found)) | |
| for i, x := range found { | |
| name := x.info.Name() | |
| rev := x.info.Revision | |
| url, err := route.URL("name", name) | |
| if err != nil { | |
| logger.Noticef("Cannot build URL for snap %q revision %s: %v", name, rev, err) | |
| continue | |
| } | |
| data, err := json.Marshal(webify(mapLocal(x), url.String())) | |
| if err != nil { | |
| return InternalError("cannot serialize snap %q revision %s: %v", name, rev, err) | |
| } | |
| raw := json.RawMessage(data) | |
| results[i] = &raw | |
| } | |
| return SyncResponse(results, &Meta{Sources: []string{"local"}}) | |
| } | |
| func resultHasType(r map[string]interface{}, allowedTypes []string) bool { | |
| for _, t := range allowedTypes { | |
| if r["type"] == t { | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| // licenseData holds details about the snap license, and may be | |
| // marshaled back as an error when the license agreement is pending, | |
| // and is expected as input to accept (or not) that license | |
| // agreement. As such, its field names are part of the API. | |
| type licenseData struct { | |
| Intro string `json:"intro"` | |
| License string `json:"license"` | |
| Agreed bool `json:"agreed"` | |
| } | |
| func (*licenseData) Error() string { | |
| return "license agreement required" | |
| } | |
| type snapInstruction struct { | |
| progress.NullMeter | |
| Action string `json:"action"` | |
| Channel string `json:"channel"` | |
| Revision snap.Revision `json:"revision"` | |
| DevMode bool `json:"devmode"` | |
| JailMode bool `json:"jailmode"` | |
| Classic bool `json:"classic"` | |
| IgnoreValidation bool `json:"ignore-validation"` | |
| Unaliased bool `json:"unaliased"` | |
| // dropping support temporarely until flag confusion is sorted, | |
| // this isn't supported by client atm anyway | |
| LeaveOld bool `json:"temp-dropped-leave-old"` | |
| License *licenseData `json:"license"` | |
| Snaps []string `json:"snaps"` | |
| // The fields below should not be unmarshalled into. Do not export them. | |
| userID int | |
| } | |
| func (inst *snapInstruction) modeFlags() (snapstate.Flags, error) { | |
| return modeFlags(inst.DevMode, inst.JailMode, inst.Classic) | |
| } | |
| func (inst *snapInstruction) installFlags() (snapstate.Flags, error) { | |
| flags, err := inst.modeFlags() | |
| if err != nil { | |
| return snapstate.Flags{}, err | |
| } | |
| if inst.Unaliased { | |
| flags.Unaliased = true | |
| } | |
| return flags, nil | |
| } | |
| var ( | |
| snapstateInstall = snapstate.Install | |
| snapstateInstallPath = snapstate.InstallPath | |
| snapstateRefreshCandidates = snapstate.RefreshCandidates | |
| snapstateTryPath = snapstate.TryPath | |
| snapstateUpdate = snapstate.Update | |
| snapstateUpdateMany = snapstate.UpdateMany | |
| snapstateInstallMany = snapstate.InstallMany | |
| snapstateRemoveMany = snapstate.RemoveMany | |
| snapstateRevert = snapstate.Revert | |
| snapstateRevertToRevision = snapstate.RevertToRevision | |
| snapstateSwitch = snapstate.Switch | |
| assertstateRefreshSnapDeclarations = assertstate.RefreshSnapDeclarations | |
| ) | |
| func ensureStateSoonImpl(st *state.State) { | |
| st.EnsureBefore(0) | |
| } | |
| var ensureStateSoon = ensureStateSoonImpl | |
| var errNothingToInstall = errors.New("nothing to install") | |
| var errDevJailModeConflict = errors.New("cannot use devmode and jailmode flags together") | |
| var errClassicDevmodeConflict = errors.New("cannot use classic and devmode flags together") | |
| var errNoJailMode = errors.New("this system cannot honour the jailmode flag") | |
| func modeFlags(devMode, jailMode, classic bool) (snapstate.Flags, error) { | |
| flags := snapstate.Flags{} | |
| devModeOS := release.ReleaseInfo.ForceDevMode() | |
| switch { | |
| case jailMode && devModeOS: | |
| return flags, errNoJailMode | |
| case jailMode && devMode: | |
| return flags, errDevJailModeConflict | |
| case devMode && classic: | |
| return flags, errClassicDevmodeConflict | |
| } | |
| // NOTE: jailmode and classic are allowed together. In that setting, | |
| // jailmode overrides classic and the app gets regular (non-classic) | |
| // confinement. | |
| flags.JailMode = jailMode | |
| flags.Classic = classic | |
| flags.DevMode = devMode | |
| return flags, nil | |
| } | |
| func snapUpdateMany(inst *snapInstruction, st *state.State) (msg string, updated []string, tasksets []*state.TaskSet, err error) { | |
| // we need refreshed snap-declarations to enforce refresh-control as best as we can, this also ensures that snap-declarations and their prerequisite assertions are updated regularly | |
| if err := assertstateRefreshSnapDeclarations(st, inst.userID); err != nil { | |
| return "", nil, nil, err | |
| } | |
| updated, tasksets, err = snapstateUpdateMany(st, inst.Snaps, inst.userID) | |
| if err != nil { | |
| return "", nil, nil, err | |
| } | |
| switch len(updated) { | |
| case 0: | |
| if len(inst.Snaps) != 0 { | |
| // TRANSLATORS: the %s is a comma-separated list of quoted snap names | |
| msg = fmt.Sprintf(i18n.G("Refresh snaps %s: no updates"), strutil.Quoted(inst.Snaps)) | |
| } else { | |
| msg = fmt.Sprintf(i18n.G("Refresh all snaps: no updates")) | |
| } | |
| case 1: | |
| msg = fmt.Sprintf(i18n.G("Refresh snap %q"), updated[0]) | |
| default: | |
| quoted := strutil.Quoted(updated) | |
| // TRANSLATORS: the %s is a comma-separated list of quoted snap names | |
| msg = fmt.Sprintf(i18n.G("Refresh snaps %s"), quoted) | |
| } | |
| return msg, updated, tasksets, nil | |
| } | |
| func verifySnapInstructions(inst *snapInstruction) error { | |
| switch inst.Action { | |
| case "install": | |
| for _, snapName := range inst.Snaps { | |
| // FIXME: alternatively we could simply mutate *inst | |
| // and s/ubuntu-core/core/ ? | |
| if snapName == "ubuntu-core" { | |
| return fmt.Errorf(`cannot install "ubuntu-core", please use "core" instead`) | |
| } | |
| } | |
| } | |
| return nil | |
| } | |
| func snapInstallMany(inst *snapInstruction, st *state.State) (msg string, installed []string, tasksets []*state.TaskSet, err error) { | |
| installed, tasksets, err = snapstateInstallMany(st, inst.Snaps, inst.userID) | |
| if err != nil { | |
| return "", nil, nil, err | |
| } | |
| switch len(inst.Snaps) { | |
| case 0: | |
| return "", nil, nil, fmt.Errorf("cannot install zero snaps") | |
| case 1: | |
| msg = fmt.Sprintf(i18n.G("Install snap %q"), inst.Snaps[0]) | |
| default: | |
| quoted := strutil.Quoted(inst.Snaps) | |
| // TRANSLATORS: the %s is a comma-separated list of quoted snap names | |
| msg = fmt.Sprintf(i18n.G("Install snaps %s"), quoted) | |
| } | |
| return msg, installed, tasksets, nil | |
| } | |
| func snapInstall(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { | |
| flags, err := inst.installFlags() | |
| if err != nil { | |
| return "", nil, err | |
| } | |
| logger.Noticef("Installing snap %q revision %s", inst.Snaps[0], inst.Revision) | |
| tset, err := snapstateInstall(st, inst.Snaps[0], inst.Channel, inst.Revision, inst.userID, flags) | |
| if err != nil { | |
| return "", nil, err | |
| } | |
| msg := fmt.Sprintf(i18n.G("Install %q snap"), inst.Snaps[0]) | |
| if inst.Channel != "stable" && inst.Channel != "" { | |
| msg = fmt.Sprintf(i18n.G("Install %q snap from %q channel"), inst.Snaps[0], inst.Channel) | |
| } | |
| return msg, []*state.TaskSet{tset}, nil | |
| } | |
| func snapUpdate(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { | |
| // TODO: bail if revision is given (and != current?), *or* behave as with install --revision? | |
| flags, err := inst.modeFlags() | |
| if err != nil { | |
| return "", nil, err | |
| } | |
| if inst.IgnoreValidation { | |
| flags.IgnoreValidation = true | |
| } | |
| // we need refreshed snap-declarations to enforce refresh-control as best as we can | |
| if err = assertstateRefreshSnapDeclarations(st, inst.userID); err != nil { | |
| return "", nil, err | |
| } | |
| ts, err := snapstateUpdate(st, inst.Snaps[0], inst.Channel, inst.Revision, inst.userID, flags) | |
| if err != nil { | |
| return "", nil, err | |
| } | |
| msg := fmt.Sprintf(i18n.G("Refresh %q snap"), inst.Snaps[0]) | |
| if inst.Channel != "stable" && inst.Channel != "" { | |
| msg = fmt.Sprintf(i18n.G("Refresh %q snap from %q channel"), inst.Snaps[0], inst.Channel) | |
| } | |
| return msg, []*state.TaskSet{ts}, nil | |
| } | |
| func snapRemoveMany(inst *snapInstruction, st *state.State) (msg string, removed []string, tasksets []*state.TaskSet, err error) { | |
| removed, tasksets, err = snapstateRemoveMany(st, inst.Snaps) | |
| if err != nil { | |
| return "", nil, nil, err | |
| } | |
| switch len(inst.Snaps) { | |
| case 0: | |
| return "", nil, nil, fmt.Errorf("cannot remove zero snaps") | |
| case 1: | |
| msg = fmt.Sprintf(i18n.G("Remove snap %q"), inst.Snaps[0]) | |
| default: | |
| quoted := strutil.Quoted(inst.Snaps) | |
| // TRANSLATORS: the %s is a comma-separated list of quoted snap names | |
| msg = fmt.Sprintf(i18n.G("Remove snaps %s"), quoted) | |
| } | |
| return msg, removed, tasksets, nil | |
| } | |
| func snapRemove(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { | |
| ts, err := snapstate.Remove(st, inst.Snaps[0], inst.Revision) | |
| if err != nil { | |
| return "", nil, err | |
| } | |
| msg := fmt.Sprintf(i18n.G("Remove %q snap"), inst.Snaps[0]) | |
| return msg, []*state.TaskSet{ts}, nil | |
| } | |
| func snapRevert(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { | |
| var ts *state.TaskSet | |
| flags, err := inst.modeFlags() | |
| if err != nil { | |
| return "", nil, err | |
| } | |
| if inst.Revision.Unset() { | |
| ts, err = snapstateRevert(st, inst.Snaps[0], flags) | |
| } else { | |
| ts, err = snapstateRevertToRevision(st, inst.Snaps[0], inst.Revision, flags) | |
| } | |
| if err != nil { | |
| return "", nil, err | |
| } | |
| msg := fmt.Sprintf(i18n.G("Revert %q snap"), inst.Snaps[0]) | |
| return msg, []*state.TaskSet{ts}, nil | |
| } | |
| func snapEnable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { | |
| if !inst.Revision.Unset() { | |
| return "", nil, errors.New("enable takes no revision") | |
| } | |
| ts, err := snapstate.Enable(st, inst.Snaps[0]) | |
| if err != nil { | |
| return "", nil, err | |
| } | |
| msg := fmt.Sprintf(i18n.G("Enable %q snap"), inst.Snaps[0]) | |
| return msg, []*state.TaskSet{ts}, nil | |
| } | |
| func snapDisable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { | |
| if !inst.Revision.Unset() { | |
| return "", nil, errors.New("disable takes no revision") | |
| } | |
| ts, err := snapstate.Disable(st, inst.Snaps[0]) | |
| if err != nil { | |
| return "", nil, err | |
| } | |
| msg := fmt.Sprintf(i18n.G("Disable %q snap"), inst.Snaps[0]) | |
| return msg, []*state.TaskSet{ts}, nil | |
| } | |
| func snapSwitch(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { | |
| if !inst.Revision.Unset() { | |
| return "", nil, errors.New("switch takes no revision") | |
| } | |
| ts, err := snapstate.Switch(st, inst.Snaps[0], inst.Channel) | |
| if err != nil { | |
| return "", nil, err | |
| } | |
| msg := fmt.Sprintf(i18n.G("Switch %q snap to %s"), inst.Snaps[0], inst.Channel) | |
| return msg, []*state.TaskSet{ts}, nil | |
| } | |
| type snapActionFunc func(*snapInstruction, *state.State) (string, []*state.TaskSet, error) | |
| var snapInstructionDispTable = map[string]snapActionFunc{ | |
| "install": snapInstall, | |
| "refresh": snapUpdate, | |
| "remove": snapRemove, | |
| "revert": snapRevert, | |
| "enable": snapEnable, | |
| "disable": snapDisable, | |
| "switch": snapSwitch, | |
| } | |
| func (inst *snapInstruction) dispatch() snapActionFunc { | |
| if len(inst.Snaps) != 1 { | |
| logger.Panicf("dispatch only handles single-snap ops; got %d", len(inst.Snaps)) | |
| } | |
| return snapInstructionDispTable[inst.Action] | |
| } | |
| func (inst *snapInstruction) errToResponse(err error) Response { | |
| var kind errorKind | |
| switch err { | |
| case store.ErrSnapNotFound: | |
| return SnapNotFound(inst.Snaps[0], err) | |
| case store.ErrNoUpdateAvailable: | |
| kind = errorKindSnapNoUpdateAvailable | |
| case store.ErrLocalSnap: | |
| kind = errorKindSnapLocal | |
| default: | |
| switch err := err.(type) { | |
| case *snap.AlreadyInstalledError: | |
| kind = errorKindSnapAlreadyInstalled | |
| case *snap.NotInstalledError: | |
| kind = errorKindSnapNotInstalled | |
| case *snapstate.SnapNeedsDevModeError: | |
| kind = errorKindSnapNeedsDevMode | |
| case *snapstate.SnapNeedsClassicError: | |
| kind = errorKindSnapNeedsClassic | |
| case *snapstate.SnapNeedsClassicSystemError: | |
| kind = errorKindSnapNeedsClassicSystem | |
| default: | |
| return BadRequest("cannot %s %q: %v", inst.Action, inst.Snaps[0], err) | |
| } | |
| } | |
| return SyncResponse(&resp{ | |
| Type: ResponseTypeError, | |
| Result: &errorResult{Message: err.Error(), Kind: kind}, | |
| Status: 400, | |
| }, nil) | |
| } | |
| func postSnap(c *Command, r *http.Request, user *auth.UserState) Response { | |
| route := c.d.router.Get(stateChangeCmd.Path) | |
| if route == nil { | |
| return InternalError("cannot find route for change") | |
| } | |
| decoder := json.NewDecoder(r.Body) | |
| var inst snapInstruction | |
| if err := decoder.Decode(&inst); err != nil { | |
| return BadRequest("cannot decode request body into snap instruction: %v", err) | |
| } | |
| state := c.d.overlord.State() | |
| state.Lock() | |
| defer state.Unlock() | |
| if user != nil { | |
| inst.userID = user.ID | |
| } | |
| vars := muxVars(r) | |
| inst.Snaps = []string{vars["name"]} | |
| if err := verifySnapInstructions(&inst); err != nil { | |
| return BadRequest("%s", err) | |
| } | |
| impl := inst.dispatch() | |
| if impl == nil { | |
| return BadRequest("unknown action %s", inst.Action) | |
| } | |
| msg, tsets, err := impl(&inst, state) | |
| if err != nil { | |
| return inst.errToResponse(err) | |
| } | |
| chg := newChange(state, inst.Action+"-snap", msg, tsets, inst.Snaps) | |
| ensureStateSoon(state) | |
| return AsyncResponse(nil, &Meta{Change: chg.ID()}) | |
| } | |
| func newChange(st *state.State, kind, summary string, tsets []*state.TaskSet, snapNames []string) *state.Change { | |
| chg := st.NewChange(kind, summary) | |
| for _, ts := range tsets { | |
| chg.AddAll(ts) | |
| } | |
| if snapNames != nil { | |
| chg.Set("snap-names", snapNames) | |
| } | |
| return chg | |
| } | |
| const maxReadBuflen = 1024 * 1024 | |
| func trySnap(c *Command, r *http.Request, user *auth.UserState, trydir string, flags snapstate.Flags) Response { | |
| st := c.d.overlord.State() | |
| st.Lock() | |
| defer st.Unlock() | |
| if !filepath.IsAbs(trydir) { | |
| return BadRequest("cannot try %q: need an absolute path", trydir) | |
| } | |
| if !osutil.IsDirectory(trydir) { | |
| return BadRequest("cannot try %q: not a snap directory", trydir) | |
| } | |
| // the developer asked us to do this with a trusted snap dir | |
| info, err := unsafeReadSnapInfo(trydir) | |
| if _, ok := err.(snap.NotSnapError); ok { | |
| return SyncResponse(&resp{ | |
| Type: ResponseTypeError, | |
| Result: &errorResult{ | |
| Message: err.Error(), | |
| Kind: errorKindNotSnap, | |
| }, | |
| Status: 400, | |
| }, nil) | |
| } | |
| if err != nil { | |
| return BadRequest("cannot read snap info for %s: %s", trydir, err) | |
| } | |
| tset, err := snapstateTryPath(st, info.Name(), trydir, flags) | |
| if err != nil { | |
| return BadRequest("cannot try %s: %s", trydir, err) | |
| } | |
| msg := fmt.Sprintf(i18n.G("Try %q snap from %s"), info.Name(), trydir) | |
| chg := newChange(st, "try-snap", msg, []*state.TaskSet{tset}, []string{info.Name()}) | |
| chg.Set("api-data", map[string]string{"snap-name": info.Name()}) | |
| ensureStateSoon(st) | |
| return AsyncResponse(nil, &Meta{Change: chg.ID()}) | |
| } | |
| func isTrue(form *multipart.Form, key string) bool { | |
| value := form.Value[key] | |
| if len(value) == 0 { | |
| return false | |
| } | |
| b, err := strconv.ParseBool(value[0]) | |
| if err != nil { | |
| return false | |
| } | |
| return b | |
| } | |
| func snapsOp(c *Command, r *http.Request, user *auth.UserState) Response { | |
| route := c.d.router.Get(stateChangeCmd.Path) | |
| if route == nil { | |
| return InternalError("cannot find route for change") | |
| } | |
| decoder := json.NewDecoder(r.Body) | |
| var inst snapInstruction | |
| if err := decoder.Decode(&inst); err != nil { | |
| return BadRequest("cannot decode request body into snap instruction: %v", err) | |
| } | |
| if inst.Channel != "" || !inst.Revision.Unset() || inst.DevMode || inst.JailMode { | |
| return BadRequest("unsupported option provided for multi-snap operation") | |
| } | |
| st := c.d.overlord.State() | |
| st.Lock() | |
| defer st.Unlock() | |
| if user != nil { | |
| inst.userID = user.ID | |
| } | |
| var msg string | |
| var affected []string | |
| var tsets []*state.TaskSet | |
| var err error | |
| switch inst.Action { | |
| case "refresh": | |
| msg, affected, tsets, err = snapUpdateMany(&inst, st) | |
| case "install": | |
| msg, affected, tsets, err = snapInstallMany(&inst, st) | |
| case "remove": | |
| msg, affected, tsets, err = snapRemoveMany(&inst, st) | |
| default: | |
| return BadRequest("unsupported multi-snap operation %q", inst.Action) | |
| } | |
| if err != nil { | |
| return InternalError("cannot %s %q: %v", inst.Action, inst.Snaps, err) | |
| } | |
| var chg *state.Change | |
| if len(tsets) == 0 { | |
| chg = st.NewChange(inst.Action+"-snap", msg) | |
| chg.SetStatus(state.DoneStatus) | |
| } else { | |
| chg = newChange(st, inst.Action+"-snap", msg, tsets, affected) | |
| ensureStateSoon(st) | |
| } | |
| chg.Set("api-data", map[string]interface{}{"snap-names": affected}) | |
| return AsyncResponse(nil, &Meta{Change: chg.ID()}) | |
| } | |
| func postSnaps(c *Command, r *http.Request, user *auth.UserState) Response { | |
| contentType := r.Header.Get("Content-Type") | |
| if contentType == "application/json" { | |
| return snapsOp(c, r, user) | |
| } | |
| if !strings.HasPrefix(contentType, "multipart/") { | |
| return BadRequest("unknown content type: %s", contentType) | |
| } | |
| route := c.d.router.Get(stateChangeCmd.Path) | |
| if route == nil { | |
| return InternalError("cannot find route for change") | |
| } | |
| // POSTs to sideload snaps must be a multipart/form-data file upload. | |
| _, params, err := mime.ParseMediaType(contentType) | |
| if err != nil { | |
| return BadRequest("cannot parse POST body: %v", err) | |
| } | |
| form, err := multipart.NewReader(r.Body, params["boundary"]).ReadForm(maxReadBuflen) | |
| if err != nil { | |
| return BadRequest("cannot read POST form: %v", err) | |
| } | |
| dangerousOK := isTrue(form, "dangerous") | |
| flags, err := modeFlags(isTrue(form, "devmode"), isTrue(form, "jailmode"), isTrue(form, "classic")) | |
| if err != nil { | |
| return BadRequest(err.Error()) | |
| } | |
| if len(form.Value["action"]) > 0 && form.Value["action"][0] == "try" { | |
| if len(form.Value["snap-path"]) == 0 { | |
| return BadRequest("need 'snap-path' value in form") | |
| } | |
| return trySnap(c, r, user, form.Value["snap-path"][0], flags) | |
| } | |
| flags.RemoveSnapPath = true | |
| // find the file for the "snap" form field | |
| var snapBody multipart.File | |
| var origPath string | |
| out: | |
| for name, fheaders := range form.File { | |
| if name != "snap" { | |
| continue | |
| } | |
| for _, fheader := range fheaders { | |
| snapBody, err = fheader.Open() | |
| origPath = fheader.Filename | |
| if err != nil { | |
| return BadRequest(`cannot open uploaded "snap" file: %v`, err) | |
| } | |
| defer snapBody.Close() | |
| break out | |
| } | |
| } | |
| defer form.RemoveAll() | |
| if snapBody == nil { | |
| return BadRequest(`cannot find "snap" file field in provided multipart/form-data payload`) | |
| } | |
| // we are in charge of the tempfile life cycle until we hand it off to the change | |
| changeTriggered := false | |
| // if you change this prefix, look for it in the tests | |
| tmpf, err := ioutil.TempFile("", "snapd-sideload-pkg-") | |
| if err != nil { | |
| return InternalError("cannot create temporary file: %v", err) | |
| } | |
| tempPath := tmpf.Name() | |
| defer func() { | |
| if !changeTriggered { | |
| os.Remove(tempPath) | |
| } | |
| }() | |
| if _, err := io.Copy(tmpf, snapBody); err != nil { | |
| return InternalError("cannot copy request into temporary file: %v", err) | |
| } | |
| tmpf.Sync() | |
| if len(form.Value["snap-path"]) > 0 { | |
| origPath = form.Value["snap-path"][0] | |
| } | |
| st := c.d.overlord.State() | |
| st.Lock() | |
| defer st.Unlock() | |
| var snapName string | |
| var sideInfo *snap.SideInfo | |
| if !dangerousOK { | |
| si, err := snapasserts.DeriveSideInfo(tempPath, assertstate.DB(st)) | |
| switch { | |
| case err == nil: | |
| snapName = si.RealName | |
| sideInfo = si | |
| case asserts.IsNotFound(err): | |
| // with devmode we try to find assertions but it's ok | |
| // if they are not there (implies --dangerous) | |
| if !isTrue(form, "devmode") { | |
| msg := "cannot find signatures with metadata for snap" | |
| if origPath != "" { | |
| msg = fmt.Sprintf("%s %q", msg, origPath) | |
| } | |
| return BadRequest(msg) | |
| } | |
| // TODO: set a warning if devmode | |
| default: | |
| return BadRequest(err.Error()) | |
| } | |
| } | |
| if snapName == "" { | |
| // potentially dangerous but dangerous or devmode params were set | |
| info, err := unsafeReadSnapInfo(tempPath) | |
| if err != nil { | |
| return BadRequest("cannot read snap file: %v", err) | |
| } | |
| snapName = info.Name() | |
| sideInfo = &snap.SideInfo{RealName: snapName} | |
| } | |
| msg := fmt.Sprintf(i18n.G("Install %q snap from file"), snapName) | |
| if origPath != "" { | |
| msg = fmt.Sprintf(i18n.G("Install %q snap from file %q"), snapName, origPath) | |
| } | |
| tset, err := snapstateInstallPath(st, sideInfo, tempPath, "", flags) | |
| if err != nil { | |
| return InternalError("cannot install snap file: %v", err) | |
| } | |
| chg := newChange(st, "install-snap", msg, []*state.TaskSet{tset}, []string{snapName}) | |
| chg.Set("api-data", map[string]string{"snap-name": snapName}) | |
| ensureStateSoon(st) | |
| // only when the unlock succeeds (as opposed to panicing) is the handoff done | |
| // but this is good enough | |
| changeTriggered = true | |
| return AsyncResponse(nil, &Meta{Change: chg.ID()}) | |
| } | |
| func unsafeReadSnapInfoImpl(snapPath string) (*snap.Info, error) { | |
| // Condider using DeriveSideInfo before falling back to this! | |
| snapf, err := snap.Open(snapPath) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return snap.ReadInfoFromSnapFile(snapf, nil) | |
| } | |
| var unsafeReadSnapInfo = unsafeReadSnapInfoImpl | |
| func iconGet(st *state.State, name string) Response { | |
| about, err := localSnapInfo(st, name) | |
| if err != nil { | |
| if err == errNoSnap { | |
| return SnapNotFound(name, err) | |
| } | |
| return InternalError("%v", err) | |
| } | |
| path := filepath.Clean(snapIcon(about.info)) | |
| if !strings.HasPrefix(path, dirs.SnapMountDir) { | |
| // XXX: how could this happen? | |
| return BadRequest("requested icon is not in snap path") | |
| } | |
| return FileResponse(path) | |
| } | |
| func appIconGet(c *Command, r *http.Request, user *auth.UserState) Response { | |
| vars := muxVars(r) | |
| name := vars["name"] | |
| return iconGet(c.d.overlord.State(), name) | |
| } | |
| func splitQS(qs string) []string { | |
| qsl := strings.Split(qs, ",") | |
| split := make([]string, 0, len(qsl)) | |
| for _, elem := range qsl { | |
| elem = strings.TrimSpace(elem) | |
| if len(elem) > 0 { | |
| split = append(split, elem) | |
| } | |
| } | |
| return split | |
| } | |
| func getSnapConf(c *Command, r *http.Request, user *auth.UserState) Response { | |
| vars := muxVars(r) | |
| snapName := vars["name"] | |
| keys := splitQS(r.URL.Query().Get("keys")) | |
| s := c.d.overlord.State() | |
| s.Lock() | |
| tr := config.NewTransaction(s) | |
| s.Unlock() | |
| currentConfValues := make(map[string]interface{}) | |
| // Special case - return root document | |
| if len(keys) == 0 { | |
| keys = []string{""} | |
| } | |
| for _, key := range keys { | |
| var value interface{} | |
| if err := tr.Get(snapName, key, &value); err != nil { | |
| if config.IsNoOption(err) { | |
| if key == "" { | |
| // no configuration - return empty document | |
| currentConfValues = make(map[string]interface{}) | |
| break | |
| } | |
| return BadRequest("%v", err) | |
| } else { | |
| return InternalError("%v", err) | |
| } | |
| } | |
| if key == "" { | |
| if len(keys) > 1 { | |
| return BadRequest("keys contains zero-length string") | |
| } | |
| return SyncResponse(value, nil) | |
| } | |
| currentConfValues[key] = value | |
| } | |
| return SyncResponse(currentConfValues, nil) | |
| } | |
| func setSnapConf(c *Command, r *http.Request, user *auth.UserState) Response { | |
| vars := muxVars(r) | |
| snapName := vars["name"] | |
| var patchValues map[string]interface{} | |
| if err := jsonutil.DecodeWithNumber(r.Body, &patchValues); err != nil { | |
| return BadRequest("cannot decode request body into patch values: %v", err) | |
| } | |
| st := c.d.overlord.State() | |
| st.Lock() | |
| defer st.Unlock() | |
| taskset, err := configstate.ConfigureInstalled(st, snapName, patchValues, 0) | |
| if err != nil { | |
| if _, ok := err.(*snap.NotInstalledError); ok { | |
| return SnapNotFound(snapName, err) | |
| } | |
| return InternalError("%v", err) | |
| } | |
| summary := fmt.Sprintf("Change configuration of %q snap", snapName) | |
| change := newChange(st, "configure-snap", summary, []*state.TaskSet{taskset}, []string{snapName}) | |
| st.EnsureBefore(0) | |
| return AsyncResponse(nil, &Meta{Change: change.ID()}) | |
| } | |
| // interfacesConnectionsMultiplexer multiplexes to either legacy (connection) or modern behavior (interfaces). | |
| func interfacesConnectionsMultiplexer(c *Command, r *http.Request, user *auth.UserState) Response { | |
| query := r.URL.Query() | |
| qselect := query.Get("select") | |
| if qselect == "" { | |
| return getLegacyConnections(c, r, user) | |
| } else { | |
| return getInterfaces(c, r, user) | |
| } | |
| } | |
| func getInterfaces(c *Command, r *http.Request, user *auth.UserState) Response { | |
| q := r.URL.Query() | |
| pselect := q.Get("select") | |
| if pselect != "all" && pselect != "connected" { | |
| return BadRequest("unsupported select qualifier") | |
| } | |
| var names []string | |
| namesStr := q.Get("names") | |
| if namesStr != "" { | |
| names = strings.Split(namesStr, ",") | |
| } | |
| opts := &interfaces.InfoOptions{ | |
| Names: names, | |
| Doc: q.Get("doc") == "true", | |
| Plugs: q.Get("plugs") == "true", | |
| Slots: q.Get("slots") == "true", | |
| Connected: pselect == "connected", | |
| } | |
| repo := c.d.overlord.InterfaceManager().Repository() | |
| return SyncResponse(repo.Info(opts), nil) | |
| } | |
| func getLegacyConnections(c *Command, r *http.Request, user *auth.UserState) Response { | |
| repo := c.d.overlord.InterfaceManager().Repository() | |
| ifaces := repo.Interfaces() | |
| var ifjson interfacesJSON | |
| plugConns := map[string][]interfaces.SlotRef{} | |
| slotConns := map[string][]interfaces.PlugRef{} | |
| for _, conn := range ifaces.Connections { | |
| plugRef := conn.PlugRef.String() | |
| slotRef := conn.SlotRef.String() | |
| plugConns[plugRef] = append(plugConns[plugRef], conn.SlotRef) | |
| slotConns[slotRef] = append(slotConns[slotRef], conn.PlugRef) | |
| } | |
| for _, plug := range ifaces.Plugs { | |
| var apps []string | |
| for _, app := range plug.Apps { | |
| apps = append(apps, app.Name) | |
| } | |
| pj := plugJSON{ | |
| Snap: plug.Snap.Name(), | |
| Name: plug.Name, | |
| Interface: plug.Interface, | |
| Attrs: plug.Attrs, | |
| Apps: apps, | |
| Label: plug.Label, | |
| Connections: plugConns[plug.String()], | |
| } | |
| ifjson.Plugs = append(ifjson.Plugs, pj) | |
| } | |
| for _, slot := range ifaces.Slots { | |
| var apps []string | |
| for _, app := range slot.Apps { | |
| apps = append(apps, app.Name) | |
| } | |
| sj := slotJSON{ | |
| Snap: slot.Snap.Name(), | |
| Name: slot.Name, | |
| Interface: slot.Interface, | |
| Attrs: slot.Attrs, | |
| Apps: apps, | |
| Label: slot.Label, | |
| Connections: slotConns[slot.String()], | |
| } | |
| ifjson.Slots = append(ifjson.Slots, sj) | |
| } | |
| return SyncResponse(ifjson, nil) | |
| } | |
| // plugJSON aids in marshaling Plug into JSON. | |
| type plugJSON struct { | |
| Snap string `json:"snap"` | |
| Name string `json:"plug"` | |
| Interface string `json:"interface"` | |
| Attrs map[string]interface{} `json:"attrs,omitempty"` | |
| Apps []string `json:"apps,omitempty"` | |
| Label string `json:"label"` | |
| Connections []interfaces.SlotRef `json:"connections,omitempty"` | |
| } | |
| // slotJSON aids in marshaling Slot into JSON. | |
| type slotJSON struct { | |
| Snap string `json:"snap"` | |
| Name string `json:"slot"` | |
| Interface string `json:"interface"` | |
| Attrs map[string]interface{} `json:"attrs,omitempty"` | |
| Apps []string `json:"apps,omitempty"` | |
| Label string `json:"label"` | |
| Connections []interfaces.PlugRef `json:"connections,omitempty"` | |
| } | |
| // interfacesJSON aids in marshaling plugs, slots and their connections into JSON. | |
| type interfacesJSON struct { | |
| Plugs []plugJSON `json:"plugs,omitempty"` | |
| Slots []slotJSON `json:"slots,omitempty"` | |
| } | |
| // interfaceAction is an action performed on the interface system. | |
| type interfaceAction struct { | |
| Action string `json:"action"` | |
| Plugs []plugJSON `json:"plugs,omitempty"` | |
| Slots []slotJSON `json:"slots,omitempty"` | |
| } | |
| func snapNamesFromConns(conns []interfaces.ConnRef) []string { | |
| m := make(map[string]bool) | |
| for _, conn := range conns { | |
| m[conn.PlugRef.Snap] = true | |
| m[conn.SlotRef.Snap] = true | |
| } | |
| l := make([]string, 0, len(m)) | |
| for name := range m { | |
| l = append(l, name) | |
| } | |
| sort.Strings(l) | |
| return l | |
| } | |
| // changeInterfaces controls the interfaces system. | |
| // Plugs can be connected to and disconnected from slots. | |
| // When enableInternalInterfaceActions is true plugs and slots can also be | |
| // explicitly added and removed. | |
| func changeInterfaces(c *Command, r *http.Request, user *auth.UserState) Response { | |
| var a interfaceAction | |
| decoder := json.NewDecoder(r.Body) | |
| if err := decoder.Decode(&a); err != nil { | |
| return BadRequest("cannot decode request body into an interface action: %v", err) | |
| } | |
| if a.Action == "" { | |
| return BadRequest("interface action not specified") | |
| } | |
| if !c.d.enableInternalInterfaceActions && a.Action != "connect" && a.Action != "disconnect" { | |
| return BadRequest("internal interface actions are disabled") | |
| } | |
| if len(a.Plugs) > 1 || len(a.Slots) > 1 { | |
| return NotImplemented("many-to-many operations are not implemented") | |
| } | |
| if a.Action != "connect" && a.Action != "disconnect" { | |
| return BadRequest("unsupported interface action: %q", a.Action) | |
| } | |
| if len(a.Plugs) == 0 || len(a.Slots) == 0 { | |
| return BadRequest("at least one plug and slot is required") | |
| } | |
| var summary string | |
| var err error | |
| var tasksets []*state.TaskSet | |
| var affected []string | |
| st := c.d.overlord.State() | |
| st.Lock() | |
| defer st.Unlock() | |
| switch a.Action { | |
| case "connect": | |
| var connRef interfaces.ConnRef | |
| repo := c.d.overlord.InterfaceManager().Repository() | |
| connRef, err = repo.ResolveConnect(a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name) | |
| if err == nil { | |
| var ts *state.TaskSet | |
| summary = fmt.Sprintf("Connect %s:%s to %s:%s", connRef.PlugRef.Snap, connRef.PlugRef.Name, connRef.SlotRef.Snap, connRef.SlotRef.Name) | |
| ts, err = ifacestate.Connect(st, connRef.PlugRef.Snap, connRef.PlugRef.Name, connRef.SlotRef.Snap, connRef.SlotRef.Name) | |
| tasksets = append(tasksets, ts) | |
| affected = snapNamesFromConns([]interfaces.ConnRef{connRef}) | |
| } | |
| case "disconnect": | |
| var conns []interfaces.ConnRef | |
| repo := c.d.overlord.InterfaceManager().Repository() | |
| summary = fmt.Sprintf("Disconnect %s:%s from %s:%s", a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name) | |
| conns, err = repo.ResolveDisconnect(a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name) | |
| if err == nil { | |
| for _, connRef := range conns { | |
| var ts *state.TaskSet | |
| ts, err = ifacestate.Disconnect(st, connRef.PlugRef.Snap, connRef.PlugRef.Name, connRef.SlotRef.Snap, connRef.SlotRef.Name) | |
| if err != nil { | |
| break | |
| } | |
| ts.JoinLane(st.NewLane()) | |
| tasksets = append(tasksets, ts) | |
| } | |
| affected = snapNamesFromConns(conns) | |
| } | |
| } | |
| if err != nil { | |
| return BadRequest("%v", err) | |
| } | |
| change := newChange(st, a.Action+"-snap", summary, tasksets, affected) | |
| st.EnsureBefore(0) | |
| return AsyncResponse(nil, &Meta{Change: change.ID()}) | |
| } | |
| func getAssertTypeNames(c *Command, r *http.Request, user *auth.UserState) Response { | |
| return SyncResponse(map[string][]string{ | |
| "types": asserts.TypeNames(), | |
| }, nil) | |
| } | |
| func doAssert(c *Command, r *http.Request, user *auth.UserState) Response { | |
| batch := assertstate.NewBatch() | |
| _, err := batch.AddStream(r.Body) | |
| if err != nil { | |
| return BadRequest("cannot decode request body into assertions: %v", err) | |
| } | |
| state := c.d.overlord.State() | |
| state.Lock() | |
| defer state.Unlock() | |
| if err := batch.Commit(state); err != nil { | |
| return BadRequest("assert failed: %v", err) | |
| } | |
| // TODO: what more info do we want to return on success? | |
| return &resp{ | |
| Type: ResponseTypeSync, | |
| Status: 200, | |
| } | |
| } | |
| func assertsFindMany(c *Command, r *http.Request, user *auth.UserState) Response { | |
| assertTypeName := muxVars(r)["assertType"] | |
| assertType := asserts.Type(assertTypeName) | |
| if assertType == nil { | |
| return BadRequest("invalid assert type: %q", assertTypeName) | |
| } | |
| headers := map[string]string{} | |
| q := r.URL.Query() | |
| for k := range q { | |
| headers[k] = q.Get(k) | |
| } | |
| state := c.d.overlord.State() | |
| state.Lock() | |
| db := assertstate.DB(state) | |
| state.Unlock() | |
| assertions, err := db.FindMany(assertType, headers) | |
| if asserts.IsNotFound(err) { | |
| return AssertResponse(nil, true) | |
| } else if err != nil { | |
| return InternalError("searching assertions failed: %v", err) | |
| } | |
| return AssertResponse(assertions, true) | |
| } | |
| type changeInfo struct { | |
| ID string `json:"id"` | |
| Kind string `json:"kind"` | |
| Summary string `json:"summary"` | |
| Status string `json:"status"` | |
| Tasks []*taskInfo `json:"tasks,omitempty"` | |
| Ready bool `json:"ready"` | |
| Err string `json:"err,omitempty"` | |
| SpawnTime time.Time `json:"spawn-time,omitempty"` | |
| ReadyTime *time.Time `json:"ready-time,omitempty"` | |
| Data map[string]*json.RawMessage `json:"data,omitempty"` | |
| } | |
| type taskInfo struct { | |
| ID string `json:"id"` | |
| Kind string `json:"kind"` | |
| Summary string `json:"summary"` | |
| Status string `json:"status"` | |
| Log []string `json:"log,omitempty"` | |
| Progress taskInfoProgress `json:"progress"` | |
| SpawnTime time.Time `json:"spawn-time,omitempty"` | |
| ReadyTime *time.Time `json:"ready-time,omitempty"` | |
| } | |
| type taskInfoProgress struct { | |
| Label string `json:"label"` | |
| Done int `json:"done"` | |
| Total int `json:"total"` | |
| } | |
| func change2changeInfo(chg *state.Change) *changeInfo { | |
| status := chg.Status() | |
| chgInfo := &changeInfo{ | |
| ID: chg.ID(), | |
| Kind: chg.Kind(), | |
| Summary: chg.Summary(), | |
| Status: status.String(), | |
| Ready: status.Ready(), | |
| SpawnTime: chg.SpawnTime(), | |
| } | |
| readyTime := chg.ReadyTime() | |
| if !readyTime.IsZero() { | |
| chgInfo.ReadyTime = &readyTime | |
| } | |
| if err := chg.Err(); err != nil { | |
| chgInfo.Err = err.Error() | |
| } | |
| tasks := chg.Tasks() | |
| taskInfos := make([]*taskInfo, len(tasks)) | |
| for j, t := range tasks { | |
| label, done, total := t.Progress() | |
| taskInfo := &taskInfo{ | |
| ID: t.ID(), | |
| Kind: t.Kind(), | |
| Summary: t.Summary(), | |
| Status: t.Status().String(), | |
| Log: t.Log(), | |
| Progress: taskInfoProgress{ | |
| Label: label, | |
| Done: done, | |
| Total: total, | |
| }, | |
| SpawnTime: t.SpawnTime(), | |
| } | |
| readyTime := t.ReadyTime() | |
| if !readyTime.IsZero() { | |
| taskInfo.ReadyTime = &readyTime | |
| } | |
| taskInfos[j] = taskInfo | |
| } | |
| chgInfo.Tasks = taskInfos | |
| var data map[string]*json.RawMessage | |
| if chg.Get("api-data", &data) == nil { | |
| chgInfo.Data = data | |
| } | |
| return chgInfo | |
| } | |
| func getChange(c *Command, r *http.Request, user *auth.UserState) Response { | |
| chID := muxVars(r)["id"] | |
| state := c.d.overlord.State() | |
| state.Lock() | |
| defer state.Unlock() | |
| chg := state.Change(chID) | |
| if chg == nil { | |
| return NotFound("cannot find change with id %q", chID) | |
| } | |
| return SyncResponse(change2changeInfo(chg), nil) | |
| } | |
| func getChanges(c *Command, r *http.Request, user *auth.UserState) Response { | |
| query := r.URL.Query() | |
| qselect := query.Get("select") | |
| if qselect == "" { | |
| qselect = "in-progress" | |
| } | |
| var filter func(*state.Change) bool | |
| switch qselect { | |
| case "all": | |
| filter = func(*state.Change) bool { return true } | |
| case "in-progress": | |
| filter = func(chg *state.Change) bool { return !chg.Status().Ready() } | |
| case "ready": | |
| filter = func(chg *state.Change) bool { return chg.Status().Ready() } | |
| default: | |
| return BadRequest("select should be one of: all,in-progress,ready") | |
| } | |
| if wantedName := query.Get("for"); wantedName != "" { | |
| outerFilter := filter | |
| filter = func(chg *state.Change) bool { | |
| if !outerFilter(chg) { | |
| return false | |
| } | |
| var snapNames []string | |
| if err := chg.Get("snap-names", &snapNames); err != nil { | |
| logger.Noticef("Cannot get snap-name for change %v", chg.ID()) | |
| return false | |
| } | |
| for _, snapName := range snapNames { | |
| if snapName == wantedName { | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| } | |
| state := c.d.overlord.State() | |
| state.Lock() | |
| defer state.Unlock() | |
| chgs := state.Changes() | |
| chgInfos := make([]*changeInfo, 0, len(chgs)) | |
| for _, chg := range chgs { | |
| if !filter(chg) { | |
| continue | |
| } | |
| chgInfos = append(chgInfos, change2changeInfo(chg)) | |
| } | |
| return SyncResponse(chgInfos, nil) | |
| } | |
| func abortChange(c *Command, r *http.Request, user *auth.UserState) Response { | |
| chID := muxVars(r)["id"] | |
| state := c.d.overlord.State() | |
| state.Lock() | |
| defer state.Unlock() | |
| chg := state.Change(chID) | |
| if chg == nil { | |
| return NotFound("cannot find change with id %q", chID) | |
| } | |
| var reqData struct { | |
| Action string `json:"action"` | |
| } | |
| decoder := json.NewDecoder(r.Body) | |
| if err := decoder.Decode(&reqData); err != nil { | |
| return BadRequest("cannot decode data from request body: %v", err) | |
| } | |
| if reqData.Action != "abort" { | |
| return BadRequest("change action %q is unsupported", reqData.Action) | |
| } | |
| if chg.Status().Ready() { | |
| return BadRequest("cannot abort change %s with nothing pending", chID) | |
| } | |
| // flag the change | |
| chg.Abort() | |
| // actually ask to proceed with the abort | |
| ensureStateSoon(state) | |
| return SyncResponse(change2changeInfo(chg), nil) | |
| } | |
| var ( | |
| postCreateUserUcrednetGet = ucrednetGet | |
| storeUserInfo = store.UserInfo | |
| osutilAddUser = osutil.AddUser | |
| ) | |
| func getUserDetailsFromStore(email string) (string, *osutil.AddUserOptions, error) { | |
| v, err := storeUserInfo(email) | |
| if err != nil { | |
| return "", nil, fmt.Errorf("cannot create user %q: %s", email, err) | |
| } | |
| if len(v.SSHKeys) == 0 { | |
| return "", nil, fmt.Errorf("cannot create user for %q: no ssh keys found", email) | |
| } | |
| gecos := fmt.Sprintf("%s,%s", email, v.OpenIDIdentifier) | |
| opts := &osutil.AddUserOptions{ | |
| SSHKeys: v.SSHKeys, | |
| Gecos: gecos, | |
| } | |
| return v.Username, opts, nil | |
| } | |
| func createAllKnownSystemUsers(st *state.State, createData *postUserCreateData) Response { | |
| var createdUsers []userResponseData | |
| st.Lock() | |
| db := assertstate.DB(st) | |
| modelAs, err := devicestate.Model(st) | |
| st.Unlock() | |
| if err != nil { | |
| return InternalError("cannot get model assertion") | |
| } | |
| headers := map[string]string{ | |
| "brand-id": modelAs.BrandID(), | |
| } | |
| st.Lock() | |
| assertions, err := db.FindMany(asserts.SystemUserType, headers) | |
| st.Unlock() | |
| if err != nil && !asserts.IsNotFound(err) { | |
| return BadRequest("cannot find system-user assertion: %s", err) | |
| } | |
| for _, as := range assertions { | |
| email := as.(*asserts.SystemUser).Email() | |
| // we need to use getUserDetailsFromAssertion as this verifies | |
| // the assertion against the current brand/model/time | |
| username, opts, err := getUserDetailsFromAssertion(st, email) | |
| if err != nil { | |
| logger.Noticef("ignoring system-user assertion for %q: %s", email, err) | |
| continue | |
| } | |
| // ignore already existing users | |
| if _, err := user.Lookup(username); err == nil { | |
| continue | |
| } | |
| // FIXME: duplicated code | |
| opts.Sudoer = createData.Sudoer | |
| opts.ExtraUsers = !release.OnClassic | |
| if err := osutilAddUser(username, opts); err != nil { | |
| return InternalError("cannot add user %q: %s", username, err) | |
| } | |
| if err := setupLocalUser(st, username, email); err != nil { | |
| return InternalError("%s", err) | |
| } | |
| createdUsers = append(createdUsers, userResponseData{ | |
| Username: username, | |
| SSHKeys: opts.SSHKeys, | |
| }) | |
| } | |
| return SyncResponse(createdUsers, nil) | |
| } | |
| func getUserDetailsFromAssertion(st *state.State, email string) (string, *osutil.AddUserOptions, error) { | |
| errorPrefix := fmt.Sprintf("cannot add system-user %q: ", email) | |
| st.Lock() | |
| db := assertstate.DB(st) | |
| modelAs, err := devicestate.Model(st) | |
| st.Unlock() | |
| if err != nil { | |
| return "", nil, fmt.Errorf(errorPrefix+"cannot get model assertion: %s", err) | |
| } | |
| brandID := modelAs.BrandID() | |
| series := modelAs.Series() | |
| model := modelAs.Model() | |
| a, err := db.Find(asserts.SystemUserType, map[string]string{ | |
| "brand-id": brandID, | |
| "email": email, | |
| }) | |
| if err != nil { | |
| return "", nil, fmt.Errorf(errorPrefix+"%v", err) | |
| } | |
| // the asserts package guarantees that this cast will work | |
| su := a.(*asserts.SystemUser) | |
| // cross check that the assertion is valid for the given series/model | |
| // check that the signer of the assertion is one of the accepted ones | |
| sysUserAuths := modelAs.SystemUserAuthority() | |
| if len(sysUserAuths) > 0 && !strutil.ListContains(sysUserAuths, su.AuthorityID()) { | |
| return "", nil, fmt.Errorf(errorPrefix+"%q not in accepted authorities %q", email, su.AuthorityID(), sysUserAuths) | |
| } | |
| if len(su.Series()) > 0 && !strutil.ListContains(su.Series(), series) { | |
| return "", nil, fmt.Errorf(errorPrefix+"%q not in series %q", email, series, su.Series()) | |
| } | |
| if len(su.Models()) > 0 && !strutil.ListContains(su.Models(), model) { | |
| return "", nil, fmt.Errorf(errorPrefix+"%q not in models %q", model, su.Models()) | |
| } | |
| if !su.ValidAt(time.Now()) { | |
| return "", nil, fmt.Errorf(errorPrefix + "assertion not valid anymore") | |
| } | |
| gecos := fmt.Sprintf("%s,%s", email, su.Name()) | |
| opts := &osutil.AddUserOptions{ | |
| SSHKeys: su.SSHKeys(), | |
| Gecos: gecos, | |
| Password: su.Password(), | |
| } | |
| return su.Username(), opts, nil | |
| } | |
| type postUserCreateData struct { | |
| Email string `json:"email"` | |
| Sudoer bool `json:"sudoer"` | |
| Known bool `json:"known"` | |
| ForceManaged bool `json:"force-managed"` | |
| } | |
| var userLookup = user.Lookup | |
| func setupLocalUser(st *state.State, username, email string) error { | |
| user, err := userLookup(username) | |
| if err != nil { | |
| return fmt.Errorf("cannot lookup user %q: %s", username, err) | |
| } | |
| uid, gid, err := osutil.UidGid(user) | |
| if err != nil { | |
| return err | |
| } | |
| authDataFn := filepath.Join(user.HomeDir, ".snap", "auth.json") | |
| if err := osutil.MkdirAllChown(filepath.Dir(authDataFn), 0700, uid, gid); err != nil { | |
| return err | |
| } | |
| // setup new user, local-only | |
| st.Lock() | |
| authUser, err := auth.NewUser(st, username, email, "", nil) | |
| st.Unlock() | |
| if err != nil { | |
| return fmt.Errorf("cannot persist authentication details: %v", err) | |
| } | |
| // store macaroon auth in auth.json in the new users home dir | |
| outStr, err := json.Marshal(struct { | |
| Macaroon string `json:"macaroon"` | |
| }{ | |
| Macaroon: authUser.Macaroon, | |
| }) | |
| if err != nil { | |
| return fmt.Errorf("cannot marshal auth data: %s", err) | |
| } | |
| if err := osutil.AtomicWriteFileChown(authDataFn, []byte(outStr), 0600, 0, uid, gid); err != nil { | |
| return fmt.Errorf("cannot write auth file %q: %s", authDataFn, err) | |
| } | |
| return nil | |
| } | |
| func postCreateUser(c *Command, r *http.Request, user *auth.UserState) Response { | |
| _, uid, err := postCreateUserUcrednetGet(r.RemoteAddr) | |
| if err != nil { | |
| return BadRequest("cannot get ucrednet uid: %v", err) | |
| } | |
| if uid != 0 { | |
| return BadRequest("cannot use create-user as non-root") | |
| } | |
| var createData postUserCreateData | |
| decoder := json.NewDecoder(r.Body) | |
| if err := decoder.Decode(&createData); err != nil { | |
| return BadRequest("cannot decode create-user data from request body: %v", err) | |
| } | |
| // verify request | |
| st := c.d.overlord.State() | |
| st.Lock() | |
| users, err := auth.Users(st) | |
| st.Unlock() | |
| if err != nil { | |
| return InternalError("cannot get user count: %s", err) | |
| } | |
| if !createData.ForceManaged { | |
| if len(users) > 0 { | |
| return BadRequest("cannot create user: device already managed") | |
| } | |
| if release.OnClassic { | |
| return BadRequest("cannot create user: device is a classic system") | |
| } | |
| } | |
| // special case: the user requested the creation of all known | |
| // system-users | |
| if createData.Email == "" && createData.Known { | |
| return createAllKnownSystemUsers(c.d.overlord.State(), &createData) | |
| } | |
| if createData.Email == "" { | |
| return BadRequest("cannot create user: 'email' field is empty") | |
| } | |
| var username string | |
| var opts *osutil.AddUserOptions | |
| if createData.Known { | |
| username, opts, err = getUserDetailsFromAssertion(st, createData.Email) | |
| } else { | |
| username, opts, err = getUserDetailsFromStore(createData.Email) | |
| } | |
| if err != nil { | |
| return BadRequest("%s", err) | |
| } | |
| // FIXME: duplicated code | |
| opts.Sudoer = createData.Sudoer | |
| opts.ExtraUsers = !release.OnClassic | |
| if err := osutilAddUser(username, opts); err != nil { | |
| return BadRequest("cannot create user %s: %s", username, err) | |
| } | |
| if err := setupLocalUser(c.d.overlord.State(), username, createData.Email); err != nil { | |
| return InternalError("%s", err) | |
| } | |
| return SyncResponse(&userResponseData{ | |
| Username: username, | |
| SSHKeys: opts.SSHKeys, | |
| }, nil) | |
| } | |
| func convertBuyError(err error) Response { | |
| switch err { | |
| case nil: | |
| return nil | |
| case store.ErrInvalidCredentials: | |
| return Unauthorized(err.Error()) | |
| case store.ErrUnauthenticated: | |
| return SyncResponse(&resp{ | |
| Type: ResponseTypeError, | |
| Result: &errorResult{ | |
| Message: err.Error(), | |
| Kind: errorKindLoginRequired, | |
| }, | |
| Status: 400, | |
| }, nil) | |
| case store.ErrTOSNotAccepted: | |
| return SyncResponse(&resp{ | |
| Type: ResponseTypeError, | |
| Result: &errorResult{ | |
| Message: err.Error(), | |
| Kind: errorKindTermsNotAccepted, | |
| }, | |
| Status: 400, | |
| }, nil) | |
| case store.ErrNoPaymentMethods: | |
| return SyncResponse(&resp{ | |
| Type: ResponseTypeError, | |
| Result: &errorResult{ | |
| Message: err.Error(), | |
| Kind: errorKindNoPaymentMethods, | |
| }, | |
| Status: 400, | |
| }, nil) | |
| case store.ErrPaymentDeclined: | |
| return SyncResponse(&resp{ | |
| Type: ResponseTypeError, | |
| Result: &errorResult{ | |
| Message: err.Error(), | |
| Kind: errorKindPaymentDeclined, | |
| }, | |
| Status: 400, | |
| }, nil) | |
| default: | |
| return InternalError("%v", err) | |
| } | |
| } | |
| type debugAction struct { | |
| Action string `json:"action"` | |
| } | |
| func postDebug(c *Command, r *http.Request, user *auth.UserState) Response { | |
| var a debugAction | |
| decoder := json.NewDecoder(r.Body) | |
| if err := decoder.Decode(&a); err != nil { | |
| return BadRequest("cannot decode request body into a debug action: %v", err) | |
| } | |
| st := c.d.overlord.State() | |
| st.Lock() | |
| defer st.Unlock() | |
| switch a.Action { | |
| case "ensure-state-soon": | |
| ensureStateSoon(st) | |
| return SyncResponse(true, nil) | |
| case "get-base-declaration": | |
| bd, err := assertstate.BaseDeclaration(st) | |
| if err != nil { | |
| return InternalError("cannot get base declaration: %s", err) | |
| } | |
| return SyncResponse(map[string]interface{}{ | |
| "base-declaration": string(asserts.Encode(bd)), | |
| }, nil) | |
| case "can-manage-refreshes": | |
| return SyncResponse(devicestate.CanManageRefreshes(st), nil) | |
| default: | |
| return BadRequest("unknown debug action: %v", a.Action) | |
| } | |
| } | |
| func postBuy(c *Command, r *http.Request, user *auth.UserState) Response { | |
| var opts store.BuyOptions | |
| decoder := json.NewDecoder(r.Body) | |
| err := decoder.Decode(&opts) | |
| if err != nil { | |
| return BadRequest("cannot decode buy options from request body: %v", err) | |
| } | |
| s := getStore(c) | |
| buyResult, err := s.Buy(&opts, user) | |
| if resp := convertBuyError(err); resp != nil { | |
| return resp | |
| } | |
| return SyncResponse(buyResult, nil) | |
| } | |
| func readyToBuy(c *Command, r *http.Request, user *auth.UserState) Response { | |
| s := getStore(c) | |
| if resp := convertBuyError(s.ReadyToBuy(user)); resp != nil { | |
| return resp | |
| } | |
| return SyncResponse(true, nil) | |
| } | |
| func runSnapctl(c *Command, r *http.Request, user *auth.UserState) Response { | |
| var snapctlOptions client.SnapCtlOptions | |
| if err := jsonutil.DecodeWithNumber(r.Body, &snapctlOptions); err != nil { | |
| return BadRequest("cannot decode snapctl request: %s", err) | |
| } | |
| if len(snapctlOptions.Args) == 0 { | |
| return BadRequest("snapctl cannot run without args") | |
| } | |
| // Ignore missing context error to allow 'snapctl -h' without a context; | |
| // Actual context is validated later by get/set. | |
| context, _ := c.d.overlord.HookManager().Context(snapctlOptions.ContextID) | |
| stdout, stderr, err := ctlcmd.Run(context, snapctlOptions.Args) | |
| if err != nil { | |
| if e, ok := err.(*flags.Error); ok && e.Type == flags.ErrHelp { | |
| stdout = []byte(e.Error()) | |
| } else { | |
| return BadRequest("error running snapctl: %s", err) | |
| } | |
| } | |
| if context != nil && context.IsEphemeral() { | |
| context.Lock() | |
| defer context.Unlock() | |
| if err := context.Done(); err != nil { | |
| return BadRequest(i18n.G("set failed: %v"), err) | |
| } | |
| } | |
| result := map[string]string{ | |
| "stdout": string(stdout), | |
| "stderr": string(stderr), | |
| } | |
| return SyncResponse(result, nil) | |
| } | |
| func getUsers(c *Command, r *http.Request, user *auth.UserState) Response { | |
| _, uid, err := postCreateUserUcrednetGet(r.RemoteAddr) | |
| if err != nil { | |
| return BadRequest("cannot get ucrednet uid: %v", err) | |
| } | |
| if uid != 0 { | |
| return BadRequest("cannot get users as non-root") | |
| } | |
| st := c.d.overlord.State() | |
| st.Lock() | |
| users, err := auth.Users(st) | |
| st.Unlock() | |
| if err != nil { | |
| return InternalError("cannot get users: %s", err) | |
| } | |
| resp := make([]userResponseData, len(users)) | |
| for i, u := range users { | |
| resp[i] = userResponseData{ | |
| Username: u.Username, | |
| Email: u.Email, | |
| ID: u.ID, | |
| } | |
| } | |
| return SyncResponse(resp, nil) | |
| } | |
| // aliasAction is an action performed on aliases | |
| type aliasAction struct { | |
| Action string `json:"action"` | |
| Snap string `json:"snap"` | |
| App string `json:"app"` | |
| Alias string `json:"alias"` | |
| // old now unsupported api | |
| Aliases []string `json:"aliases"` | |
| } | |
| func changeAliases(c *Command, r *http.Request, user *auth.UserState) Response { | |
| var a aliasAction | |
| decoder := json.NewDecoder(r.Body) | |
| if err := decoder.Decode(&a); err != nil { | |
| return BadRequest("cannot decode request body into an alias action: %v", err) | |
| } | |
| if len(a.Aliases) != 0 { | |
| return BadRequest("cannot interpret request, snaps can no longer be expected to declare their aliases") | |
| } | |
| var taskset *state.TaskSet | |
| var err error | |
| st := c.d.overlord.State() | |
| st.Lock() | |
| defer st.Unlock() | |
| switch a.Action { | |
| default: | |
| return BadRequest("unsupported alias action: %q", a.Action) | |
| case "alias": | |
| taskset, err = snapstate.Alias(st, a.Snap, a.App, a.Alias) | |
| case "unalias": | |
| if a.Alias == a.Snap { | |
| // Do What I mean: | |
| // check if a snap is referred/intended | |
| // or just an alias | |
| var snapst snapstate.SnapState | |
| err := snapstate.Get(st, a.Snap, &snapst) | |
| if err != nil && err != state.ErrNoState { | |
| return InternalError("%v", err) | |
| } | |
| if err == state.ErrNoState { // not a snap | |
| a.Snap = "" | |
| } | |
| } | |
| if a.Snap != "" { | |
| a.Alias = "" | |
| taskset, err = snapstate.DisableAllAliases(st, a.Snap) | |
| } else { | |
| taskset, a.Snap, err = snapstate.RemoveManualAlias(st, a.Alias) | |
| } | |
| case "prefer": | |
| taskset, err = snapstate.Prefer(st, a.Snap) | |
| } | |
| if err != nil { | |
| return BadRequest("%v", err) | |
| } | |
| var summary string | |
| switch a.Action { | |
| case "alias": | |
| summary = fmt.Sprintf(i18n.G("Setup alias %q => %q for snap %q"), a.Alias, a.App, a.Snap) | |
| case "unalias": | |
| if a.Alias != "" { | |
| summary = fmt.Sprintf(i18n.G("Remove manual alias %q for snap %q"), a.Alias, a.Snap) | |
| } else { | |
| summary = fmt.Sprintf(i18n.G("Disable all aliases for snap %q"), a.Snap) | |
| } | |
| case "prefer": | |
| summary = fmt.Sprintf(i18n.G("Prefer aliases of snap %q"), a.Snap) | |
| } | |
| change := newChange(st, a.Action, summary, []*state.TaskSet{taskset}, []string{a.Snap}) | |
| st.EnsureBefore(0) | |
| return AsyncResponse(nil, &Meta{Change: change.ID()}) | |
| } | |
| type aliasStatus struct { | |
| Command string `json:"command"` | |
| Status string `json:"status"` | |
| Manual string `json:"manual,omitempty"` | |
| Auto string `json:"auto,omitempty"` | |
| } | |
| // getAliases produces a response with a map snap -> alias -> aliasStatus | |
| func getAliases(c *Command, r *http.Request, user *auth.UserState) Response { | |
| state := c.d.overlord.State() | |
| state.Lock() | |
| defer state.Unlock() | |
| res := make(map[string]map[string]aliasStatus) | |
| allStates, err := snapstate.All(state) | |
| if err != nil { | |
| return InternalError("cannot list local snaps: %v", err) | |
| } | |
| for snapName, snapst := range allStates { | |
| if err != nil { | |
| return InternalError("cannot retrieve info for snap %q: %v", snapName, err) | |
| } | |
| if len(snapst.Aliases) != 0 { | |
| snapAliases := make(map[string]aliasStatus) | |
| res[snapName] = snapAliases | |
| autoDisabled := snapst.AutoAliasesDisabled | |
| for alias, aliasTarget := range snapst.Aliases { | |
| aliasStatus := aliasStatus{ | |
| Manual: aliasTarget.Manual, | |
| Auto: aliasTarget.Auto, | |
| } | |
| status := "auto" | |
| tgt := aliasTarget.Effective(autoDisabled) | |
| if tgt == "" { | |
| status = "disabled" | |
| tgt = aliasTarget.Auto | |
| } else if aliasTarget.Manual != "" { | |
| status = "manual" | |
| } | |
| aliasStatus.Status = status | |
| aliasStatus.Command = snap.JoinSnapApp(snapName, tgt) | |
| snapAliases[alias] = aliasStatus | |
| } | |
| } | |
| } | |
| return SyncResponse(res, nil) | |
| } | |
| func getAppsInfo(c *Command, r *http.Request, user *auth.UserState) Response { | |
| query := r.URL.Query() | |
| opts := appInfoOptions{} | |
| switch sel := query.Get("select"); sel { | |
| case "": | |
| // nothing to do | |
| case "service": | |
| opts.service = true | |
| default: | |
| return BadRequest("invalid select parameter: %q", sel) | |
| } | |
| appInfos, rsp := appInfosFor(c.d.overlord.State(), splitQS(query.Get("names")), opts) | |
| if rsp != nil { | |
| return rsp | |
| } | |
| return SyncResponse(clientAppInfosFromSnapAppInfos(appInfos), nil) | |
| } | |
| func getLogs(c *Command, r *http.Request, user *auth.UserState) Response { | |
| query := r.URL.Query() | |
| n := "10" | |
| if s := query.Get("n"); s != "" { | |
| m, err := strconv.ParseInt(s, 0, 32) | |
| if err != nil { | |
| return BadRequest(`invalid value for n: %q: %v`, s, err) | |
| } | |
| if m < 0 { | |
| n = "all" | |
| } else { | |
| n = s | |
| } | |
| } | |
| follow := false | |
| if s := query.Get("follow"); s != "" { | |
| f, err := strconv.ParseBool(s) | |
| if err != nil { | |
| return BadRequest(`invalid value for follow: %q: %v`, s, err) | |
| } | |
| follow = f | |
| } | |
| // only services have logs for now | |
| opts := appInfoOptions{service: true} | |
| appInfos, rsp := appInfosFor(c.d.overlord.State(), splitQS(query.Get("names")), opts) | |
| if rsp != nil { | |
| return rsp | |
| } | |
| if len(appInfos) == 0 { | |
| return AppNotFound("no matching services") | |
| } | |
| serviceNames := make([]string, len(appInfos)) | |
| for i, appInfo := range appInfos { | |
| serviceNames[i] = appInfo.ServiceName() | |
| } | |
| sysd := systemd.New(dirs.GlobalRootDir, progress.Null) | |
| reader, err := sysd.LogReader(serviceNames, n, follow) | |
| if err != nil { | |
| return InternalError("cannot get logs: %v", err) | |
| } | |
| return &journalLineReaderSeqResponse{ | |
| ReadCloser: reader, | |
| follow: follow, | |
| } | |
| } | |
| func postApps(c *Command, r *http.Request, user *auth.UserState) Response { | |
| var inst servicestate.Instruction | |
| decoder := json.NewDecoder(r.Body) | |
| if err := decoder.Decode(&inst); err != nil { | |
| return BadRequest("cannot decode request body into service operation: %v", err) | |
| } | |
| if len(inst.Names) == 0 { | |
| // on POST, don't allow empty to mean all | |
| return BadRequest("cannot perform operation on services without a list of services to operate on") | |
| } | |
| st := c.d.overlord.State() | |
| appInfos, rsp := appInfosFor(st, inst.Names, appInfoOptions{service: true}) | |
| if rsp != nil { | |
| return rsp | |
| } | |
| if len(appInfos) == 0 { | |
| // can't happen: appInfosFor with a non-empty list of services | |
| // shouldn't ever return an empty appInfos with no error response | |
| return InternalError("no services found") | |
| } | |
| ts, err := servicestate.Control(st, appInfos, &inst, nil) | |
| if err != nil { | |
| if _, ok := err.(servicestate.ServiceActionConflictError); ok { | |
| return Conflict(err.Error()) | |
| } | |
| return BadRequest(err.Error()) | |
| } | |
| st.Lock() | |
| defer st.Unlock() | |
| chg := newChange(st, "service-control", fmt.Sprintf("Running service command"), []*state.TaskSet{ts}, inst.Names) | |
| st.EnsureBefore(0) | |
| return AsyncResponse(nil, &Meta{Change: chg.ID()}) | |
| } |