Permalink
Fetching contributors…
Cannot retrieve contributors at this time
1703 lines (1464 sloc) 49.7 KB
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2016-2017 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 snapstate implements the manager and state aspects responsible for the installation and removal of snaps.
package snapstate
import (
"encoding/json"
"fmt"
"reflect"
"github.com/snapcore/snapd/boot"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/interfaces"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/overlord/snapstate/backend"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/store"
)
// control flags for doInstall
const (
maybeCore = 1 << iota
skipConfigure
)
// control flags for "Configure()"
const (
IgnoreHookError = 1 << iota
TrackHookError
UseConfigDefaults
)
func needsMaybeCore(typ snap.Type) int {
if typ == snap.TypeOS {
return maybeCore
}
return 0
}
func doInstall(st *state.State, snapst *SnapState, snapsup *SnapSetup, flags int) (*state.TaskSet, error) {
if snapst.IsInstalled() && !snapst.Active {
return nil, fmt.Errorf("cannot update disabled snap %q", snapsup.Name())
}
if snapsup.Flags.Classic {
if !release.OnClassic {
return nil, fmt.Errorf("classic confinement is only supported on classic systems")
} else if !dirs.SupportsClassicConfinement() {
return nil, fmt.Errorf(i18n.G("classic confinement requires snaps under /snap or symlink from /snap to %s"), dirs.SnapMountDir)
}
}
if !snapst.IsInstalled() { // install?
// check that the snap command namespace doesn't conflict with an enabled alias
if err := checkSnapAliasConflict(st, snapsup.Name()); err != nil {
return nil, err
}
}
if err := CheckChangeConflict(st, snapsup.Name(), nil, snapst); err != nil {
return nil, err
}
// ensure core gets installed. if it is already installed return
// an empty task set
ts := state.NewTaskSet()
targetRevision := snapsup.Revision()
revisionStr := ""
if snapsup.SideInfo != nil {
revisionStr = fmt.Sprintf(" (%s)", targetRevision)
}
// check if we already have the revision locally (alters tasks)
revisionIsLocal := snapst.LastIndex(targetRevision) >= 0
prereq := st.NewTask("prerequisites", fmt.Sprintf(i18n.G("Ensure prerequisites for %q are available"), snapsup.Name()))
prereq.Set("snap-setup", snapsup)
var prepare, prev *state.Task
fromStore := false
// if we have a local revision here we go back to that
if snapsup.SnapPath != "" || revisionIsLocal {
prepare = st.NewTask("prepare-snap", fmt.Sprintf(i18n.G("Prepare snap %q%s"), snapsup.SnapPath, revisionStr))
} else {
fromStore = true
prepare = st.NewTask("download-snap", fmt.Sprintf(i18n.G("Download snap %q%s from channel %q"), snapsup.Name(), revisionStr, snapsup.Channel))
}
prepare.Set("snap-setup", snapsup)
prepare.WaitFor(prereq)
tasks := []*state.Task{prereq, prepare}
addTask := func(t *state.Task) {
t.Set("snap-setup-task", prepare.ID())
t.WaitFor(prev)
tasks = append(tasks, t)
}
prev = prepare
if fromStore {
// fetch and check assertions
checkAsserts := st.NewTask("validate-snap", fmt.Sprintf(i18n.G("Fetch and check assertions for snap %q%s"), snapsup.Name(), revisionStr))
addTask(checkAsserts)
prev = checkAsserts
}
// mount
if !revisionIsLocal {
mount := st.NewTask("mount-snap", fmt.Sprintf(i18n.G("Mount snap %q%s"), snapsup.Name(), revisionStr))
addTask(mount)
prev = mount
}
// run refresh hooks when updating existing snap, otherwise run install hook further down.
runRefreshHooks := (snapst.IsInstalled() && !snapsup.Flags.Revert)
if runRefreshHooks {
preRefreshHook := SetupPreRefreshHook(st, snapsup.Name())
addTask(preRefreshHook)
prev = preRefreshHook
}
if snapst.IsInstalled() {
// unlink-current-snap (will stop services for copy-data)
stop := st.NewTask("stop-snap-services", fmt.Sprintf(i18n.G("Stop snap %q services"), snapsup.Name()))
addTask(stop)
prev = stop
removeAliases := st.NewTask("remove-aliases", fmt.Sprintf(i18n.G("Remove aliases for snap %q"), snapsup.Name()))
addTask(removeAliases)
prev = removeAliases
unlink := st.NewTask("unlink-current-snap", fmt.Sprintf(i18n.G("Make current revision for snap %q unavailable"), snapsup.Name()))
addTask(unlink)
prev = unlink
}
// copy-data (needs stopped services by unlink)
if !snapsup.Flags.Revert {
copyData := st.NewTask("copy-snap-data", fmt.Sprintf(i18n.G("Copy snap %q data"), snapsup.Name()))
addTask(copyData)
prev = copyData
}
// security
setupSecurity := st.NewTask("setup-profiles", fmt.Sprintf(i18n.G("Setup snap %q%s security profiles"), snapsup.Name(), revisionStr))
addTask(setupSecurity)
prev = setupSecurity
// finalize (wrappers+current symlink)
linkSnap := st.NewTask("link-snap", fmt.Sprintf(i18n.G("Make snap %q%s available to the system"), snapsup.Name(), revisionStr))
addTask(linkSnap)
prev = linkSnap
// security: phase 2, no-op unless core
if flags&maybeCore != 0 {
setupSecurityPhase2 := st.NewTask("setup-profiles", fmt.Sprintf(i18n.G("Setup snap %q%s security profiles (phase 2)"), snapsup.Name(), revisionStr))
setupSecurityPhase2.Set("core-phase-2", true)
addTask(setupSecurityPhase2)
prev = setupSecurityPhase2
}
// setup aliases
setAutoAliases := st.NewTask("set-auto-aliases", fmt.Sprintf(i18n.G("Set automatic aliases for snap %q"), snapsup.Name()))
addTask(setAutoAliases)
prev = setAutoAliases
setupAliases := st.NewTask("setup-aliases", fmt.Sprintf(i18n.G("Setup snap %q aliases"), snapsup.Name()))
addTask(setupAliases)
prev = setupAliases
if runRefreshHooks {
postRefreshHook := SetupPostRefreshHook(st, snapsup.Name())
addTask(postRefreshHook)
prev = postRefreshHook
}
// only run install hook if installing the snap for the first time
if !snapst.IsInstalled() {
installHook := SetupInstallHook(st, snapsup.Name())
addTask(installHook)
prev = installHook
}
// run new serices
startSnapServices := st.NewTask("start-snap-services", fmt.Sprintf(i18n.G("Start snap %q%s services"), snapsup.Name(), revisionStr))
addTask(startSnapServices)
prev = startSnapServices
// Do not do that if we are reverting to a local revision
if snapst.IsInstalled() && !snapsup.Flags.Revert {
seq := snapst.Sequence
currentIndex := snapst.LastIndex(snapst.Current)
// discard everything after "current" (we may have reverted to
// a previous versions earlier)
for i := currentIndex + 1; i < len(seq); i++ {
si := seq[i]
if si.Revision == targetRevision {
// but don't discard this one; its' the thing we're switching to!
continue
}
ts := removeInactiveRevision(st, snapsup.Name(), si.Revision)
ts.WaitFor(prev)
tasks = append(tasks, ts.Tasks()...)
prev = tasks[len(tasks)-1]
}
// make sure we're not scheduling the removal of the target
// revision in the case where the target revision is already in
// the sequence.
for i := 0; i < currentIndex; i++ {
si := seq[i]
if si.Revision == targetRevision {
// we do *not* want to removeInactiveRevision of this one
copy(seq[i:], seq[i+1:])
seq = seq[:len(seq)-1]
currentIndex--
}
}
// normal garbage collect
for i := 0; i <= currentIndex-2; i++ {
si := seq[i]
if boot.InUse(snapsup.Name(), si.Revision) {
continue
}
ts := removeInactiveRevision(st, snapsup.Name(), si.Revision)
ts.WaitFor(prev)
tasks = append(tasks, ts.Tasks()...)
prev = tasks[len(tasks)-1]
}
addTask(st.NewTask("cleanup", fmt.Sprintf("Clean up %q%s install", snapsup.Name(), revisionStr)))
}
installSet := state.NewTaskSet(tasks...)
installSet.WaitAll(ts)
ts.AddAll(installSet)
if flags&skipConfigure != 0 {
return installSet, nil
}
var confFlags int
if !snapst.IsInstalled() && snapsup.SideInfo != nil && snapsup.SideInfo.SnapID != "" {
// installation, run configure using the gadget defaults
// if available
confFlags |= UseConfigDefaults
}
configSet := ConfigureSnap(st, snapsup.Name(), confFlags)
configSet.WaitAll(ts)
ts.AddAll(configSet)
return ts, nil
}
// ConfigureSnap returns a set of tasks to configure snapName as done during installation/refresh.
func ConfigureSnap(st *state.State, snapName string, confFlags int) *state.TaskSet {
// This is slightly ugly, ideally we would check the type instead
// of hardcoding the name here. Unfortunately we do not have the
// type until we actually run the change.
if snapName == defaultCoreSnapName {
confFlags |= IgnoreHookError
confFlags |= TrackHookError
}
return Configure(st, snapName, nil, confFlags)
}
var Configure = func(st *state.State, snapName string, patch map[string]interface{}, flags int) *state.TaskSet {
panic("internal error: snapstate.Configure is unset")
}
var SetupInstallHook = func(st *state.State, snapName string) *state.Task {
panic("internal error: snapstate.SetupInstallHook is unset")
}
var SetupPreRefreshHook = func(st *state.State, snapName string) *state.Task {
panic("internal error: snapstate.SetupPreRefreshHook is unset")
}
var SetupPostRefreshHook = func(st *state.State, snapName string) *state.Task {
panic("internal error: snapstate.SetupPostRefreshHook is unset")
}
var SetupRemoveHook = func(st *state.State, snapName string) *state.Task {
panic("internal error: snapstate.SetupRemoveHook is unset")
}
// snapTopicalTasks are tasks that characterize changes on a snap that
// cannot be run concurrently and should conflict with each other.
var snapTopicalTasks = map[string]bool{
"link-snap": true,
"unlink-snap": true,
"switch-snap": true,
"switch-snap-channel": true,
"toggle-snap-flags": true,
"refresh-aliases": true,
"prune-auto-aliases": true,
"alias": true,
"unalias": true,
"disable-aliases": true,
"prefer-aliases": true,
"connect": true,
"disconnect": true,
}
func getPlugAndSlotRefs(task *state.Task) (*interfaces.PlugRef, *interfaces.SlotRef, error) {
var plugRef interfaces.PlugRef
var slotRef interfaces.SlotRef
if err := task.Get("plug", &plugRef); err != nil {
return nil, nil, err
}
if err := task.Get("slot", &slotRef); err != nil {
return nil, nil, err
}
return &plugRef, &slotRef, nil
}
type changeConflictError struct {
snapName string
}
func (e changeConflictError) Error() string {
return fmt.Sprintf("snap %q has changes in progress", e.snapName)
}
// CheckChangeConflictMany ensures that for the given snapNames no other
// changes that alters the snaps (like remove, install, refresh) are in
// progress. If a conflict is detected an error is returned.
//
// It's like CheckChangeConflict, but for multiple snaps, and does not
// check snapst.
func CheckChangeConflictMany(st *state.State, snapNames []string, checkConflictPredicate func(task *state.Task) bool) error {
snapMap := make(map[string]bool, len(snapNames))
for _, k := range snapNames {
snapMap[k] = true
}
for _, chg := range st.Changes() {
if chg.Status().Ready() {
continue
}
if chg.Kind() == "transition-ubuntu-core" {
return fmt.Errorf("ubuntu-core to core transition in progress, no other changes allowed until this is done")
}
}
for _, task := range st.Tasks() {
k := task.Kind()
chg := task.Change()
if snapTopicalTasks[k] && (chg == nil || !chg.Status().Ready()) {
if k == "connect" || k == "disconnect" {
plugRef, slotRef, err := getPlugAndSlotRefs(task)
if err != nil {
return fmt.Errorf("internal error: cannot obtain plug/slot data from task: %s", task.Summary())
}
if (snapMap[plugRef.Snap] || snapMap[slotRef.Snap]) && (checkConflictPredicate == nil || checkConflictPredicate(task)) {
var snapName string
if snapMap[plugRef.Snap] {
snapName = plugRef.Snap
} else {
snapName = slotRef.Snap
}
return changeConflictError{snapName}
}
} else {
snapsup, err := TaskSnapSetup(task)
if err != nil {
return fmt.Errorf("internal error: cannot obtain snap setup from task: %s", task.Summary())
}
snapName := snapsup.Name()
if (snapMap[snapName]) && (checkConflictPredicate == nil || checkConflictPredicate(task)) {
return changeConflictError{snapName}
}
}
}
}
return nil
}
type changeDuringInstallError struct {
snapName string
}
func (c changeDuringInstallError) Error() string {
return fmt.Sprintf("snap %q state changed during install preparations", c.snapName)
}
// CheckChangeConflict ensures that for the given snapName no other
// changes that alters the snap (like remove, install, refresh) are in
// progress. It also ensures that snapst (if not nil) did not get
// modified. If a conflict is detected an error is returned.
func CheckChangeConflict(st *state.State, snapName string, checkConflictPredicate func(task *state.Task) bool, snapst *SnapState) error {
if err := CheckChangeConflictMany(st, []string{snapName}, checkConflictPredicate); err != nil {
return err
}
if snapst != nil {
// caller wants us to also make sure the SnapState in state
// matches the one they provided. Necessary because we need to
// unlock while talking to the store, during which a change can
// sneak in (if it's before the taskset is created) (e.g. for
// install, while getting the snap info; for refresh, when
// getting what needs refreshing).
var cursnapst SnapState
if err := Get(st, snapName, &cursnapst); err != nil && err != state.ErrNoState {
return err
}
// TODO: implement the rather-boring-but-more-performant SnapState.Equals
if !reflect.DeepEqual(snapst, &cursnapst) {
return changeDuringInstallError{snapName: snapName}
}
}
return nil
}
// InstallPath returns a set of tasks for installing snap from a file path.
// Note that the state must be locked by the caller.
// The provided SideInfo can contain just a name which results in a
// local revision and sideloading, or full metadata in which case it
// the snap will appear as installed from the store.
func InstallPath(st *state.State, si *snap.SideInfo, path, channel string, flags Flags) (*state.TaskSet, error) {
name := si.RealName
if name == "" {
return nil, fmt.Errorf("internal error: snap name to install %q not provided", path)
}
var snapst SnapState
err := Get(st, name, &snapst)
if err != nil && err != state.ErrNoState {
return nil, err
}
if si.SnapID != "" {
if si.Revision.Unset() {
return nil, fmt.Errorf("internal error: snap id set to install %q but revision is unset", path)
}
}
instFlags := maybeCore
if flags.SkipConfigure {
// extract it as a doInstall flag, this is not passed
// into SnapSetup
instFlags |= skipConfigure
}
// It is ok do open the snap file here because we either
// have side info or the user passed --dangerous
info, _, err := backend.OpenSnapFile(path, si)
if err != nil {
return nil, err
}
snapsup := &SnapSetup{
Base: info.Base,
SideInfo: si,
SnapPath: path,
Channel: channel,
Flags: flags.ForSnapSetup(),
}
return doInstall(st, &snapst, snapsup, instFlags)
}
// TryPath returns a set of tasks for trying a snap from a file path.
// Note that the state must be locked by the caller.
func TryPath(st *state.State, name, path string, flags Flags) (*state.TaskSet, error) {
flags.TryMode = true
return InstallPath(st, &snap.SideInfo{RealName: name}, path, "", flags)
}
// Install returns a set of tasks for installing snap.
// Note that the state must be locked by the caller.
func Install(st *state.State, name, channel string, revision snap.Revision, userID int, flags Flags) (*state.TaskSet, error) {
if channel == "" {
channel = "stable"
}
var snapst SnapState
err := Get(st, name, &snapst)
if err != nil && err != state.ErrNoState {
return nil, err
}
if snapst.IsInstalled() {
return nil, &snap.AlreadyInstalledError{Snap: name}
}
info, err := snapInfo(st, name, channel, revision, userID)
if err != nil {
return nil, err
}
if err := validateInfoAndFlags(info, &snapst, flags); err != nil {
return nil, err
}
snapsup := &SnapSetup{
Channel: channel,
Base: info.Base,
UserID: userID,
Flags: flags.ForSnapSetup(),
DownloadInfo: &info.DownloadInfo,
SideInfo: &info.SideInfo,
}
return doInstall(st, &snapst, snapsup, needsMaybeCore(info.Type))
}
// InstallMany installs everything from the given list of names.
// Note that the state must be locked by the caller.
func InstallMany(st *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) {
installed := make([]string, 0, len(names))
tasksets := make([]*state.TaskSet, 0, len(names))
for _, name := range names {
ts, err := Install(st, name, "", snap.R(0), userID, Flags{})
// FIXME: is this expected behavior?
if _, ok := err.(*snap.AlreadyInstalledError); ok {
continue
}
if err != nil {
return nil, nil, err
}
installed = append(installed, name)
ts.JoinLane(st.NewLane())
tasksets = append(tasksets, ts)
}
return installed, tasksets, nil
}
// RefreshCandidates gets a list of candidates for update
// Note that the state must be locked by the caller.
func RefreshCandidates(st *state.State, user *auth.UserState) ([]*snap.Info, error) {
updates, _, _, err := refreshCandidates(st, nil, user, nil)
return updates, err
}
// ValidateRefreshes allows to hook validation into the handling of refresh candidates.
var ValidateRefreshes func(st *state.State, refreshes []*snap.Info, ignoreValidation map[string]bool, userID int) (validated []*snap.Info, err error)
// UpdateMany updates everything from the given list of names that the
// store says is updateable. If the list is empty, update everything.
// Note that the state must be locked by the caller.
func UpdateMany(st *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) {
user, err := userFromUserID(st, userID)
if err != nil {
return nil, nil, err
}
updates, stateByID, ignoreValidation, err := refreshCandidates(st, names, user, nil)
if err != nil {
return nil, nil, err
}
if ValidateRefreshes != nil && len(updates) != 0 {
updates, err = ValidateRefreshes(st, updates, ignoreValidation, userID)
if err != nil {
// not doing "refresh all" report the error
if len(names) != 0 {
return nil, nil, err
}
// doing "refresh all", log the problems
logger.Noticef("cannot refresh some snaps: %v", err)
}
}
params := func(update *snap.Info) (string, Flags, *SnapState) {
snapst := stateByID[update.SnapID]
return snapst.Channel, snapst.Flags, snapst
}
return doUpdate(st, names, updates, params, userID)
}
func doUpdate(st *state.State, names []string, updates []*snap.Info, params func(*snap.Info) (channel string, flags Flags, snapst *SnapState), userID int) ([]string, []*state.TaskSet, error) {
tasksets := make([]*state.TaskSet, 0, len(updates))
refreshAll := len(names) == 0
var nameSet map[string]bool
if len(names) != 0 {
nameSet = make(map[string]bool, len(names))
for _, name := range names {
nameSet[name] = true
}
}
newAutoAliases, mustPruneAutoAliases, transferTargets, err := autoAliasesUpdate(st, names, updates)
if err != nil {
return nil, nil, err
}
reportUpdated := make(map[string]bool, len(updates))
var pruningAutoAliasesTs *state.TaskSet
if len(mustPruneAutoAliases) != 0 {
var err error
pruningAutoAliasesTs, err = applyAutoAliasesDelta(st, mustPruneAutoAliases, "prune", refreshAll, func(snapName string, _ *state.TaskSet) {
if nameSet[snapName] {
reportUpdated[snapName] = true
}
})
if err != nil {
return nil, nil, err
}
tasksets = append(tasksets, pruningAutoAliasesTs)
}
// wait for the auto-alias prune tasks as needed
scheduleUpdate := func(snapName string, ts *state.TaskSet) {
if pruningAutoAliasesTs != nil && (mustPruneAutoAliases[snapName] != nil || transferTargets[snapName]) {
ts.WaitAll(pruningAutoAliasesTs)
}
reportUpdated[snapName] = true
}
for _, update := range updates {
channel, flags, snapst := params(update)
if err := validateInfoAndFlags(update, snapst, flags); err != nil {
if refreshAll {
logger.Noticef("cannot update %q: %v", update.Name(), err)
continue
}
return nil, nil, err
}
snapUserID, err := userIDForSnap(st, snapst, userID)
if err != nil {
return nil, nil, err
}
snapsup := &SnapSetup{
Channel: channel,
UserID: snapUserID,
Flags: flags.ForSnapSetup(),
DownloadInfo: &update.DownloadInfo,
SideInfo: &update.SideInfo,
}
ts, err := doInstall(st, snapst, snapsup, needsMaybeCore(update.Type))
if err != nil {
if refreshAll {
// doing "refresh all", just skip this snap
logger.Noticef("cannot refresh snap %q: %v", update.Name(), err)
continue
}
return nil, nil, err
}
ts.JoinLane(st.NewLane())
scheduleUpdate(update.Name(), ts)
tasksets = append(tasksets, ts)
}
if len(newAutoAliases) != 0 {
addAutoAliasesTs, err := applyAutoAliasesDelta(st, newAutoAliases, "refresh", refreshAll, scheduleUpdate)
if err != nil {
return nil, nil, err
}
tasksets = append(tasksets, addAutoAliasesTs)
}
updated := make([]string, 0, len(reportUpdated))
for name := range reportUpdated {
updated = append(updated, name)
}
return updated, tasksets, nil
}
func applyAutoAliasesDelta(st *state.State, delta map[string][]string, op string, refreshAll bool, linkTs func(snapName string, ts *state.TaskSet)) (*state.TaskSet, error) {
applyTs := state.NewTaskSet()
kind := "refresh-aliases"
msg := i18n.G("Refresh aliases for snap %q")
if op == "prune" {
kind = "prune-auto-aliases"
msg = i18n.G("Prune automatic aliases for snap %q")
}
for snapName, aliases := range delta {
if err := CheckChangeConflict(st, snapName, nil, nil); err != nil {
if refreshAll {
// doing "refresh all", just skip this snap
logger.Noticef("cannot %s automatic aliases for snap %q: %v", op, snapName, err)
continue
}
return nil, err
}
snapsup := &SnapSetup{
SideInfo: &snap.SideInfo{RealName: snapName},
}
alias := st.NewTask(kind, fmt.Sprintf(msg, snapsup.Name()))
alias.Set("snap-setup", &snapsup)
if op == "prune" {
alias.Set("aliases", aliases)
}
ts := state.NewTaskSet(alias)
linkTs(snapName, ts)
applyTs.AddAll(ts)
}
return applyTs, nil
}
func autoAliasesUpdate(st *state.State, names []string, updates []*snap.Info) (changed map[string][]string, mustPrune map[string][]string, transferTargets map[string]bool, err error) {
changed, dropped, err := autoAliasesDelta(st, nil)
if err != nil {
if len(names) != 0 {
// not "refresh all", error
return nil, nil, nil, err
}
// log and continue
logger.Noticef("cannot find the delta for automatic aliases for some snaps: %v", err)
}
refreshAll := len(names) == 0
// dropped alias -> snapName
droppedAliases := make(map[string][]string, len(dropped))
for snapName, aliases := range dropped {
for _, alias := range aliases {
droppedAliases[alias] = append(droppedAliases[alias], snapName)
}
}
// filter changed considering only names if set:
// we add auto-aliases only for mentioned snaps
if !refreshAll && len(changed) != 0 {
filteredChanged := make(map[string][]string, len(changed))
for _, name := range names {
if changed[name] != nil {
filteredChanged[name] = changed[name]
}
}
changed = filteredChanged
}
// mark snaps that are sources or target of transfers
transferSources := make(map[string]bool, len(dropped))
transferTargets = make(map[string]bool, len(changed))
for snapName, aliases := range changed {
for _, alias := range aliases {
if sources := droppedAliases[alias]; len(sources) != 0 {
transferTargets[snapName] = true
for _, source := range sources {
transferSources[source] = true
}
}
}
}
// snaps with updates
updating := make(map[string]bool, len(updates))
for _, info := range updates {
updating[info.Name()] = true
}
// add explicitly auto-aliases only for snaps that are not updated
for snapName := range changed {
if updating[snapName] {
delete(changed, snapName)
}
}
// prune explicitly auto-aliases only for snaps that are mentioned
// and not updated OR the source of transfers
mustPrune = make(map[string][]string, len(dropped))
for snapName := range transferSources {
mustPrune[snapName] = dropped[snapName]
}
if refreshAll {
for snapName, aliases := range dropped {
if !updating[snapName] {
mustPrune[snapName] = aliases
}
}
} else {
for _, name := range names {
if !updating[name] && dropped[name] != nil {
mustPrune[name] = dropped[name]
}
}
}
return changed, mustPrune, transferTargets, nil
}
// Switch switches a snap to a new channel
func Switch(st *state.State, name, channel string) (*state.TaskSet, error) {
var snapst SnapState
err := Get(st, name, &snapst)
if err != nil && err != state.ErrNoState {
return nil, err
}
if !snapst.IsInstalled() {
return nil, &snap.NotInstalledError{Snap: name}
}
if err := CheckChangeConflict(st, name, nil, nil); err != nil {
return nil, err
}
snapsup := &SnapSetup{
SideInfo: snapst.CurrentSideInfo(),
Channel: channel,
}
switchSnap := st.NewTask("switch-snap", fmt.Sprintf(i18n.G("Switch snap %q to %s"), snapsup.Name(), snapsup.Channel))
switchSnap.Set("snap-setup", &snapsup)
return state.NewTaskSet(switchSnap), nil
}
// Update initiates a change updating a snap.
// Note that the state must be locked by the caller.
func Update(st *state.State, name, channel string, revision snap.Revision, userID int, flags Flags) (*state.TaskSet, error) {
var snapst SnapState
err := Get(st, name, &snapst)
if err != nil && err != state.ErrNoState {
return nil, err
}
if !snapst.IsInstalled() {
return nil, &snap.NotInstalledError{Snap: name}
}
// FIXME: snaps that are not active are skipped for now
// until we know what we want to do
if !snapst.Active {
return nil, fmt.Errorf("refreshing disabled snap %q not supported", name)
}
if channel == "" {
channel = snapst.Channel
}
// TODO: make flags be per revision to avoid this logic (that
// leaves corner cases all over the place)
if !(flags.JailMode || flags.DevMode) {
flags.Classic = flags.Classic || snapst.Flags.Classic
}
var updates []*snap.Info
info, infoErr := infoForUpdate(st, &snapst, name, channel, revision, userID, flags)
switch infoErr {
case nil:
updates = append(updates, info)
case store.ErrNoUpdateAvailable:
// there may be some new auto-aliases
default:
return nil, infoErr
}
params := func(update *snap.Info) (string, Flags, *SnapState) {
return channel, flags, &snapst
}
_, tts, err := doUpdate(st, []string{name}, updates, params, userID)
if err != nil {
return nil, err
}
// see if we need to update the channel or toggle ignore-validation
if infoErr == store.ErrNoUpdateAvailable && (snapst.Channel != channel || snapst.IgnoreValidation != flags.IgnoreValidation) {
if err := CheckChangeConflict(st, name, nil, nil); err != nil {
return nil, err
}
snapsup := &SnapSetup{
SideInfo: snapst.CurrentSideInfo(),
Flags: snapst.Flags.ForSnapSetup(),
}
if snapst.Channel != channel {
// update the tracked channel
snapsup.Channel = channel
// Update the current snap channel as well. This ensures that
// the UI displays the right values.
snapsup.SideInfo.Channel = channel
switchSnap := st.NewTask("switch-snap-channel", fmt.Sprintf(i18n.G("Switch snap %q from %s to %s"), snapsup.Name(), snapst.Channel, channel))
switchSnap.Set("snap-setup", &snapsup)
switchSnapTs := state.NewTaskSet(switchSnap)
for _, ts := range tts {
switchSnapTs.WaitAll(ts)
}
tts = append(tts, switchSnapTs)
}
if snapst.IgnoreValidation != flags.IgnoreValidation {
// toggle ignore validation
snapsup.IgnoreValidation = flags.IgnoreValidation
toggle := st.NewTask("toggle-snap-flags", fmt.Sprintf(i18n.G("Toggle snap %q flags"), snapsup.Name()))
toggle.Set("snap-setup", &snapsup)
toggleTs := state.NewTaskSet(toggle)
for _, ts := range tts {
toggleTs.WaitAll(ts)
}
tts = append(tts, toggleTs)
}
}
if len(tts) == 0 && len(updates) == 0 {
// really nothing to do, return the original no-update-available error
return nil, infoErr
}
flat := state.NewTaskSet()
for _, ts := range tts {
flat.AddAll(ts)
}
return flat, nil
}
func infoForUpdate(st *state.State, snapst *SnapState, name, channel string, revision snap.Revision, userID int, flags Flags) (*snap.Info, error) {
if revision.Unset() {
// good ol' refresh
info, err := updateInfo(st, snapst, channel, flags.IgnoreValidation, userID)
if err != nil {
return nil, err
}
if err := validateInfoAndFlags(info, snapst, flags); err != nil {
return nil, err
}
if ValidateRefreshes != nil && !flags.IgnoreValidation {
_, err := ValidateRefreshes(st, []*snap.Info{info}, nil, userID)
if err != nil {
return nil, err
}
}
return info, nil
}
var sideInfo *snap.SideInfo
for _, si := range snapst.Sequence {
if si.Revision == revision {
sideInfo = si
break
}
}
if sideInfo == nil {
// refresh from given revision from store
return updateToRevisionInfo(st, snapst, channel, revision, userID)
}
// refresh-to-local
return readInfo(name, sideInfo)
}
// AutoRefreshAssertions allows to hook fetching of important assertions
// into the Autorefresh function.
var AutoRefreshAssertions func(st *state.State, userID int) error
// AutoRefresh is the wrapper that will do a refresh of all the installed
// snaps on the system. In addition to that it will also refresh important
// assertions.
func AutoRefresh(st *state.State) ([]string, []*state.TaskSet, error) {
userID := 0
if AutoRefreshAssertions != nil {
if err := AutoRefreshAssertions(st, userID); err != nil {
return nil, nil, err
}
}
return UpdateMany(st, nil, userID)
}
// Enable sets a snap to the active state
func Enable(st *state.State, name string) (*state.TaskSet, error) {
var snapst SnapState
err := Get(st, name, &snapst)
if err == state.ErrNoState {
return nil, &snap.NotInstalledError{Snap: name}
}
if err != nil {
return nil, err
}
if snapst.Active {
return nil, fmt.Errorf("snap %q already enabled", name)
}
if err := CheckChangeConflict(st, name, nil, nil); err != nil {
return nil, err
}
snapsup := &SnapSetup{
SideInfo: snapst.CurrentSideInfo(),
Flags: snapst.Flags.ForSnapSetup(),
}
prepareSnap := st.NewTask("prepare-snap", fmt.Sprintf(i18n.G("Prepare snap %q (%s)"), snapsup.Name(), snapst.Current))
prepareSnap.Set("snap-setup", &snapsup)
setupProfiles := st.NewTask("setup-profiles", fmt.Sprintf(i18n.G("Setup snap %q (%s) security profiles"), snapsup.Name(), snapst.Current))
setupProfiles.Set("snap-setup", &snapsup)
setupProfiles.WaitFor(prepareSnap)
linkSnap := st.NewTask("link-snap", fmt.Sprintf(i18n.G("Make snap %q (%s) available to the system"), snapsup.Name(), snapst.Current))
linkSnap.Set("snap-setup", &snapsup)
linkSnap.WaitFor(setupProfiles)
// setup aliases
setupAliases := st.NewTask("setup-aliases", fmt.Sprintf(i18n.G("Setup snap %q aliases"), snapsup.Name()))
setupAliases.Set("snap-setup", &snapsup)
setupAliases.WaitFor(linkSnap)
startSnapServices := st.NewTask("start-snap-services", fmt.Sprintf(i18n.G("Start snap %q (%s) services"), snapsup.Name(), snapst.Current))
startSnapServices.Set("snap-setup", &snapsup)
startSnapServices.WaitFor(setupAliases)
return state.NewTaskSet(prepareSnap, setupProfiles, linkSnap, setupAliases, startSnapServices), nil
}
// Disable sets a snap to the inactive state
func Disable(st *state.State, name string) (*state.TaskSet, error) {
var snapst SnapState
err := Get(st, name, &snapst)
if err == state.ErrNoState {
return nil, &snap.NotInstalledError{Snap: name}
}
if err != nil {
return nil, err
}
if !snapst.Active {
return nil, fmt.Errorf("snap %q already disabled", name)
}
info, err := Info(st, name, snapst.Current)
if err != nil {
return nil, err
}
if !canDisable(info) {
return nil, fmt.Errorf("snap %q cannot be disabled", name)
}
if err := CheckChangeConflict(st, name, nil, nil); err != nil {
return nil, err
}
snapsup := &SnapSetup{
SideInfo: &snap.SideInfo{
RealName: name,
Revision: snapst.Current,
},
}
stopSnapServices := st.NewTask("stop-snap-services", fmt.Sprintf(i18n.G("Stop snap %q (%s) services"), snapsup.Name(), snapst.Current))
stopSnapServices.Set("snap-setup", &snapsup)
removeAliases := st.NewTask("remove-aliases", fmt.Sprintf(i18n.G("Remove aliases for snap %q"), snapsup.Name()))
removeAliases.Set("snap-setup-task", stopSnapServices.ID())
removeAliases.WaitFor(stopSnapServices)
unlinkSnap := st.NewTask("unlink-snap", fmt.Sprintf(i18n.G("Make snap %q (%s) unavailable to the system"), snapsup.Name(), snapst.Current))
unlinkSnap.Set("snap-setup-task", stopSnapServices.ID())
unlinkSnap.WaitFor(removeAliases)
removeProfiles := st.NewTask("remove-profiles", fmt.Sprintf(i18n.G("Remove security profiles of snap %q"), snapsup.Name()))
removeProfiles.Set("snap-setup-task", stopSnapServices.ID())
removeProfiles.WaitFor(unlinkSnap)
return state.NewTaskSet(stopSnapServices, removeAliases, unlinkSnap, removeProfiles), nil
}
// canDisable verifies that a snap can be deactivated.
func canDisable(si *snap.Info) bool {
for _, importantSnapType := range []snap.Type{snap.TypeGadget, snap.TypeKernel, snap.TypeOS} {
if importantSnapType == si.Type {
return false
}
}
return true
}
// canRemove verifies that a snap can be removed.
func canRemove(si *snap.Info, snapst *SnapState, removeAll bool) bool {
// removing single revisions is generally allowed
if !removeAll {
return true
}
// required snaps cannot be removed
if snapst.Required {
return false
}
// TODO: use Required for these too
// Gadget snaps should not be removed as they are a key
// building block for Gadgets. Do not remove their last
// revision left.
if si.Type == snap.TypeGadget {
return false
}
// Allow "ubuntu-core" removals here because we might have two
// core snaps installed (ubuntu-core and core). Note that
// ideally we would only allow the removal of "ubuntu-core" if
// we know that "core" is installed too and if we are part of
// the "ubuntu-core->core" transition. But this transition
// starts automatically on startup so the window of a user
// triggering this manually is very small.
//
// Once the ubuntu-core -> core transition has landed for some
// time we can remove the two lines below.
if si.Name() == "ubuntu-core" && si.Type == snap.TypeOS {
return true
}
// You never want to remove a kernel or OS. Do not remove their
// last revision left.
if si.Type == snap.TypeKernel || si.Type == snap.TypeOS {
return false
}
// TODO: on classic likely let remove core even if active if it's only snap left.
// never remove anything that is used for booting
if boot.InUse(si.Name(), si.Revision) {
return false
}
return true
}
// Remove returns a set of tasks for removing snap.
// Note that the state must be locked by the caller.
func Remove(st *state.State, name string, revision snap.Revision) (*state.TaskSet, error) {
var snapst SnapState
err := Get(st, name, &snapst)
if err != nil && err != state.ErrNoState {
return nil, err
}
if !snapst.IsInstalled() {
return nil, &snap.NotInstalledError{Snap: name, Rev: snap.R(0)}
}
if err := CheckChangeConflict(st, name, nil, nil); err != nil {
return nil, err
}
active := snapst.Active
var removeAll bool
if revision.Unset() {
revision = snapst.Current
removeAll = true
} else {
if active {
if revision == snapst.Current {
msg := "cannot remove active revision %s of snap %q"
if len(snapst.Sequence) > 1 {
msg += " (revert first?)"
}
return nil, fmt.Errorf(msg, revision, name)
}
active = false
}
if !revisionInSequence(&snapst, revision) {
return nil, &snap.NotInstalledError{Snap: name, Rev: revision}
}
removeAll = len(snapst.Sequence) == 1
}
info, err := Info(st, name, revision)
if err != nil {
return nil, err
}
// check if this is something that can be removed
if !canRemove(info, &snapst, removeAll) {
return nil, fmt.Errorf("snap %q is not removable", name)
}
// main/current SnapSetup
snapsup := SnapSetup{
SideInfo: &snap.SideInfo{
RealName: name,
Revision: revision,
},
}
// trigger remove
full := state.NewTaskSet()
var chain *state.TaskSet
addNext := func(ts *state.TaskSet) {
if chain != nil {
ts.WaitAll(chain)
}
full.AddAll(ts)
chain = ts
}
var removeHook *state.Task
// only run remove hook if uninstalling the snap completely
if removeAll {
removeHook = SetupRemoveHook(st, snapsup.Name())
}
if active { // unlink
var prev *state.Task
stopSnapServices := st.NewTask("stop-snap-services", fmt.Sprintf(i18n.G("Stop snap %q services"), name))
stopSnapServices.Set("snap-setup", snapsup)
prev = stopSnapServices
tasks := []*state.Task{stopSnapServices}
if removeHook != nil {
tasks = append(tasks, removeHook)
removeHook.WaitFor(prev)
prev = removeHook
}
removeAliases := st.NewTask("remove-aliases", fmt.Sprintf(i18n.G("Remove aliases for snap %q"), name))
removeAliases.WaitFor(prev)
removeAliases.Set("snap-setup-task", stopSnapServices.ID())
unlink := st.NewTask("unlink-snap", fmt.Sprintf(i18n.G("Make snap %q unavailable to the system"), name))
unlink.Set("snap-setup-task", stopSnapServices.ID())
unlink.WaitFor(removeAliases)
removeSecurity := st.NewTask("remove-profiles", fmt.Sprintf(i18n.G("Remove security profile for snap %q (%s)"), name, revision))
removeSecurity.WaitFor(unlink)
removeSecurity.Set("snap-setup-task", stopSnapServices.ID())
tasks = append(tasks, removeAliases, unlink, removeSecurity)
addNext(state.NewTaskSet(tasks...))
} else if removeHook != nil {
addNext(state.NewTaskSet(removeHook))
}
if removeAll {
seq := snapst.Sequence
for i := len(seq) - 1; i >= 0; i-- {
si := seq[i]
addNext(removeInactiveRevision(st, name, si.Revision))
}
discardConns := st.NewTask("discard-conns", fmt.Sprintf(i18n.G("Discard interface connections for snap %q (%s)"), name, revision))
discardConns.Set("snap-setup", &SnapSetup{
SideInfo: &snap.SideInfo{
RealName: name,
},
})
addNext(state.NewTaskSet(discardConns))
} else {
addNext(removeInactiveRevision(st, name, revision))
}
return full, nil
}
func removeInactiveRevision(st *state.State, name string, revision snap.Revision) *state.TaskSet {
snapsup := SnapSetup{
SideInfo: &snap.SideInfo{
RealName: name,
Revision: revision,
},
}
clearData := st.NewTask("clear-snap", fmt.Sprintf(i18n.G("Remove data for snap %q (%s)"), name, revision))
clearData.Set("snap-setup", snapsup)
discardSnap := st.NewTask("discard-snap", fmt.Sprintf(i18n.G("Remove snap %q (%s) from the system"), name, revision))
discardSnap.WaitFor(clearData)
discardSnap.Set("snap-setup-task", clearData.ID())
return state.NewTaskSet(clearData, discardSnap)
}
// RemoveMany removes everything from the given list of names.
// Note that the state must be locked by the caller.
func RemoveMany(st *state.State, names []string) ([]string, []*state.TaskSet, error) {
removed := make([]string, 0, len(names))
tasksets := make([]*state.TaskSet, 0, len(names))
for _, name := range names {
ts, err := Remove(st, name, snap.R(0))
// FIXME: is this expected behavior?
if _, ok := err.(*snap.NotInstalledError); ok {
continue
}
if err != nil {
return nil, nil, err
}
removed = append(removed, name)
tasksets = append(tasksets, ts)
}
return removed, tasksets, nil
}
// Revert returns a set of tasks for reverting to the previous version of the snap.
// Note that the state must be locked by the caller.
func Revert(st *state.State, name string, flags Flags) (*state.TaskSet, error) {
var snapst SnapState
err := Get(st, name, &snapst)
if err != nil && err != state.ErrNoState {
return nil, err
}
pi := snapst.previousSideInfo()
if pi == nil {
return nil, fmt.Errorf("no revision to revert to")
}
return RevertToRevision(st, name, pi.Revision, flags)
}
func RevertToRevision(st *state.State, name string, rev snap.Revision, flags Flags) (*state.TaskSet, error) {
var snapst SnapState
err := Get(st, name, &snapst)
if err != nil && err != state.ErrNoState {
return nil, err
}
if snapst.Current == rev {
return nil, fmt.Errorf("already on requested revision")
}
if !snapst.Active {
return nil, fmt.Errorf("cannot revert inactive snaps")
}
i := snapst.LastIndex(rev)
if i < 0 {
return nil, fmt.Errorf("cannot find revision %s for snap %q", rev, name)
}
typ, err := snapst.Type()
if err != nil {
return nil, err
}
flags.Revert = true
// TODO: make flags be per revision to avoid this logic (that
// leaves corner cases all over the place)
if !(flags.JailMode || flags.DevMode || flags.Classic) {
if snapst.Flags.DevMode {
flags.DevMode = true
}
if snapst.Flags.JailMode {
flags.JailMode = true
}
if snapst.Flags.Classic {
flags.Classic = true
}
}
snapsup := &SnapSetup{
SideInfo: snapst.Sequence[i],
Flags: flags.ForSnapSetup(),
}
return doInstall(st, &snapst, snapsup, needsMaybeCore(typ))
}
// TransitionCore transitions from an old core snap name to a new core
// snap name. It is used for the ubuntu-core -> core transition (that
// is not just a rename because the two snaps have different snapIDs)
//
// Note that this function makes some assumptions like:
// - no aliases setup for both snaps
// - no data needs to be copied
// - all interfaces are absolutely identical on both new and old
// Do not use this as a general way to transition from snap A to snap B.
func TransitionCore(st *state.State, oldName, newName string) ([]*state.TaskSet, error) {
var oldSnapst, newSnapst SnapState
err := Get(st, oldName, &oldSnapst)
if err != nil && err != state.ErrNoState {
return nil, err
}
if !oldSnapst.IsInstalled() {
return nil, fmt.Errorf("cannot transition snap %q: not installed", oldName)
}
var userID int
newInfo, err := snapInfo(st, newName, oldSnapst.Channel, snap.R(0), userID)
if err != nil {
return nil, err
}
var all []*state.TaskSet
// install new core (if not already installed)
err = Get(st, newName, &newSnapst)
if err != nil && err != state.ErrNoState {
return nil, err
}
if !newSnapst.IsInstalled() {
// start by instaling the new snap
tsInst, err := doInstall(st, &newSnapst, &SnapSetup{
Channel: oldSnapst.Channel,
DownloadInfo: &newInfo.DownloadInfo,
SideInfo: &newInfo.SideInfo,
}, maybeCore)
if err != nil {
return nil, err
}
all = append(all, tsInst)
}
// then transition the interface connections over
transIf := st.NewTask("transition-ubuntu-core", fmt.Sprintf(i18n.G("Transition security profiles from %q to %q"), oldName, newName))
transIf.Set("old-name", oldName)
transIf.Set("new-name", newName)
if len(all) > 0 {
transIf.WaitAll(all[0])
}
tsTrans := state.NewTaskSet(transIf)
all = append(all, tsTrans)
// FIXME: this is just here for the tests
transIf.Set("snap-setup", &SnapSetup{
SideInfo: &snap.SideInfo{
RealName: oldName,
},
})
// then remove the old snap
tsRm, err := Remove(st, oldName, snap.R(0))
if err != nil {
return nil, err
}
tsRm.WaitFor(transIf)
all = append(all, tsRm)
return all, nil
}
// State/info accessors
// Installing returns whether there's an in-progress installation.
func Installing(st *state.State) bool {
for _, task := range st.Tasks() {
k := task.Kind()
chg := task.Change()
if k == "mount-snap" && chg != nil && !chg.Status().Ready() {
return true
}
}
return false
}
// Info returns the information about the snap with given name and revision.
// Works also for a mounted candidate snap in the process of being installed.
func Info(st *state.State, name string, revision snap.Revision) (*snap.Info, error) {
var snapst SnapState
err := Get(st, name, &snapst)
if err == state.ErrNoState {
return nil, &snap.NotInstalledError{Snap: name}
}
if err != nil {
return nil, err
}
for i := len(snapst.Sequence) - 1; i >= 0; i-- {
if si := snapst.Sequence[i]; si.Revision == revision {
return readInfo(name, si)
}
}
return nil, fmt.Errorf("cannot find snap %q at revision %s", name, revision.String())
}
// CurrentInfo returns the information about the current revision of a snap with the given name.
func CurrentInfo(st *state.State, name string) (*snap.Info, error) {
var snapst SnapState
err := Get(st, name, &snapst)
if err != nil && err != state.ErrNoState {
return nil, err
}
info, err := snapst.CurrentInfo()
if err == ErrNoCurrent {
return nil, &snap.NotInstalledError{Snap: name}
}
return info, err
}
// Get retrieves the SnapState of the given snap.
func Get(st *state.State, name string, snapst *SnapState) error {
var snaps map[string]*json.RawMessage
err := st.Get("snaps", &snaps)
if err != nil {
return err
}
raw, ok := snaps[name]
if !ok {
return state.ErrNoState
}
err = json.Unmarshal([]byte(*raw), &snapst)
if err != nil {
return fmt.Errorf("cannot unmarshal snap state: %v", err)
}
return nil
}
// All retrieves return a map from name to SnapState for all current snaps in the system state.
func All(st *state.State) (map[string]*SnapState, error) {
// XXX: result is a map because sideloaded snaps carry no name
// atm in their sideinfos
var stateMap map[string]*SnapState
if err := st.Get("snaps", &stateMap); err != nil && err != state.ErrNoState {
return nil, err
}
curStates := make(map[string]*SnapState, len(stateMap))
for snapName, snapst := range stateMap {
curStates[snapName] = snapst
}
return curStates, nil
}
// NumSnaps returns the number of installed snaps.
func NumSnaps(st *state.State) (int, error) {
var snaps map[string]*json.RawMessage
if err := st.Get("snaps", &snaps); err != nil && err != state.ErrNoState {
return -1, err
}
return len(snaps), nil
}
// Set sets the SnapState of the given snap, overwriting any earlier state.
func Set(st *state.State, name string, snapst *SnapState) {
var snaps map[string]*json.RawMessage
err := st.Get("snaps", &snaps)
if err != nil && err != state.ErrNoState {
panic("internal error: cannot unmarshal snaps state: " + err.Error())
}
if snaps == nil {
snaps = make(map[string]*json.RawMessage)
}
if snapst == nil || (len(snapst.Sequence) == 0) {
delete(snaps, name)
} else {
data, err := json.Marshal(snapst)
if err != nil {
panic("internal error: cannot marshal snap state: " + err.Error())
}
raw := json.RawMessage(data)
snaps[name] = &raw
}
st.Set("snaps", snaps)
}
// ActiveInfos returns information about all active snaps.
func ActiveInfos(st *state.State) ([]*snap.Info, error) {
var stateMap map[string]*SnapState
var infos []*snap.Info
if err := st.Get("snaps", &stateMap); err != nil && err != state.ErrNoState {
return nil, err
}
for snapName, snapst := range stateMap {
if !snapst.Active {
continue
}
snapInfo, err := snapst.CurrentInfo()
if err != nil {
logger.Noticef("cannot retrieve info for snap %q: %s", snapName, err)
continue
}
infos = append(infos, snapInfo)
}
return infos, nil
}
func infosForTypes(st *state.State, snapType snap.Type) ([]*snap.Info, error) {
var stateMap map[string]*SnapState
if err := st.Get("snaps", &stateMap); err != nil && err != state.ErrNoState {
return nil, err
}
var res []*snap.Info
for _, snapst := range stateMap {
if !snapst.IsInstalled() {
continue
}
typ, err := snapst.Type()
if err != nil {
return nil, err
}
if typ != snapType {
continue
}
si, err := snapst.CurrentInfo()
if err != nil {
return nil, err
}
res = append(res, si)
}
if len(res) == 0 {
return nil, state.ErrNoState
}
return res, nil
}
func infoForType(st *state.State, snapType snap.Type) (*snap.Info, error) {
res, err := infosForTypes(st, snapType)
if err != nil {
return nil, err
}
return res[0], nil
}
// GadgetInfo finds the current gadget snap's info.
func GadgetInfo(st *state.State) (*snap.Info, error) {
return infoForType(st, snap.TypeGadget)
}
// KernelInfo finds the current kernel snap's info.
func KernelInfo(st *state.State) (*snap.Info, error) {
return infoForType(st, snap.TypeKernel)
}
// CoreInfo finds the current OS snap's info. If both
// "core" and "ubuntu-core" is installed then "core"
// is preferred. Different core names are not supported
// currently and will result in an error.
//
// Once enough time has passed and everyone transitioned
// from ubuntu-core to core we can simplify this again
// and make it the same as the above "KernelInfo".
func CoreInfo(st *state.State) (*snap.Info, error) {
res, err := infosForTypes(st, snap.TypeOS)
if err != nil {
return nil, err
}
// a single core: just return it
if len(res) == 1 {
return res[0], nil
}
// some systems have two cores: ubuntu-core/core
// we always return "core" in this case
if len(res) == 2 {
if res[0].Name() == defaultCoreSnapName && res[1].Name() == "ubuntu-core" {
return res[0], nil
}
if res[0].Name() == "ubuntu-core" && res[1].Name() == defaultCoreSnapName {
return res[1], nil
}
return nil, fmt.Errorf("unexpected cores %q and %q", res[0].Name(), res[1].Name())
}
return nil, fmt.Errorf("unexpected number of cores, got %d", len(res))
}
// ConfigDefaults returns the configuration defaults for the snap specified in the gadget. If gadget is absent or the snap has no snap-id it returns ErrNoState.
func ConfigDefaults(st *state.State, snapName string) (map[string]interface{}, error) {
gadget, err := GadgetInfo(st)
if err != nil {
return nil, err
}
var snapst SnapState
if err := Get(st, snapName, &snapst); err != nil {
return nil, err
}
si := snapst.CurrentSideInfo()
if si.SnapID == "" {
return nil, state.ErrNoState
}
gadgetInfo, err := snap.ReadGadgetInfo(gadget, release.OnClassic)
if err != nil {
return nil, err
}
defaults, ok := gadgetInfo.Defaults[si.SnapID]
if !ok {
return nil, state.ErrNoState
}
return defaults, nil
}