Skip to content

Commit

Permalink
Merge pull request #12035 from MiguelPires/refresh-hold-overlord
Browse files Browse the repository at this point in the history
o/snapstate: extend support for holding refreshes
  • Loading branch information
MiguelPires committed Aug 24, 2022
2 parents ebc7ce1 + ec9ef76 commit 9bb978e
Show file tree
Hide file tree
Showing 16 changed files with 459 additions and 108 deletions.
4 changes: 2 additions & 2 deletions overlord/configstate/configcore/refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// +build !nomanagers

/*
* Copyright (C) 2017-2018 Canonical Ltd
* Copyright (C) 2017-2022 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
Expand Down Expand Up @@ -69,7 +69,7 @@ func validateRefreshSchedule(tr config.Conf) error {
if err != nil {
return err
}
if refreshHoldStr != "" {
if refreshHoldStr != "" && refreshHoldStr != "forever" {
if _, err := time.Parse(time.RFC3339, refreshHoldStr); err != nil {
return fmt.Errorf("refresh.hold cannot be parsed: %v", err)
}
Expand Down
10 changes: 10 additions & 0 deletions overlord/configstate/configcore/refresh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ func (s *refreshSuite) TestConfigureRefreshHoldHappy(c *C) {
c.Assert(err, IsNil)
}

func (s *refreshSuite) TestConfigureRefreshHoldForeverHappy(c *C) {
err := configcore.Run(classicDev, &mockConf{
state: s.state,
conf: map[string]interface{}{
"refresh.hold": "forever",
},
})
c.Assert(err, IsNil)
}

func (s *refreshSuite) TestConfigureRefreshHoldInvalid(c *C) {
err := configcore.Run(classicDev, &mockConf{
state: s.state,
Expand Down
6 changes: 3 additions & 3 deletions overlord/hookstate/hooks.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2017 Canonical Ltd
* Copyright (C) 2017-2022 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
Expand Down Expand Up @@ -151,7 +151,7 @@ func (h *gateAutoRefreshHookHandler) Done() (err error) {
return fmt.Errorf("cannot unlock inhibit lock for snap %s: %v", snapName, err)
}
}
return snapstate.ProceedWithRefresh(st, snapName)
return snapstate.ProceedWithRefresh(st, snapName, nil)
} else {
var ok bool
action, ok = a.(snapstate.GateAutoRefreshAction)
Expand All @@ -172,7 +172,7 @@ func (h *gateAutoRefreshHookHandler) Done() (err error) {
case snapstate.GateAutoRefreshProceed:
// for action=proceed the ctlcmd doesn't call ProceedWithRefresh
// immediately, do it here.
if err := snapstate.ProceedWithRefresh(st, snapName); err != nil {
if err := snapstate.ProceedWithRefresh(st, snapName, nil); err != nil {
return err
}
if h.refreshAppAwareness {
Expand Down
35 changes: 13 additions & 22 deletions overlord/snapstate/autorefresh.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2017-2020 Canonical Ltd
* Copyright (C) 2017-2022 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
Expand Down Expand Up @@ -55,6 +55,9 @@ const maxPostponementBuffer = 5 * 24 * time.Hour
// to "13 days" left.
const maxInhibition = 14*24*time.Hour - time.Second

// maxDuration is used to represent "forever" internally (it's 290 years).
const maxDuration = time.Duration(1<<63 - 1)

// hooks setup by devicestate
var (
CanAutoRefresh func(st *state.State) (bool, error)
Expand Down Expand Up @@ -147,37 +150,25 @@ func (m *autoRefresh) LastRefresh() (time.Time, error) {
}

// EffectiveRefreshHold returns the time until to which refreshes are
// held if refresh.hold configuration is set and accounting for the
// max postponement since the last refresh.
// held if refresh.hold configuration is set.
func (m *autoRefresh) EffectiveRefreshHold() (time.Time, error) {
var holdTime time.Time
var holdValue string

tr := config.NewTransaction(m.state)
err := tr.Get("core", "refresh.hold", &holdTime)
err := tr.Get("core", "refresh.hold", &holdValue)
if err != nil && !config.IsNoOption(err) {
return time.Time{}, err
}

// cannot hold beyond last-refresh + max-postponement
lastRefresh, err := m.LastRefresh()
if err != nil {
return time.Time{}, err
if holdValue == "forever" {
return timeNow().Add(maxDuration), nil
}
if lastRefresh.IsZero() {
seedTime, err := getTime(m.state, "seed-time")
if err != nil {

var holdTime time.Time
if holdValue != "" {
if holdTime, err = time.Parse(time.RFC3339, holdValue); err != nil {
return time.Time{}, err
}
if seedTime.IsZero() {
// no reference to know whether holding is reasonable
return time.Time{}, nil
}
lastRefresh = seedTime
}

limitTime := lastRefresh.Add(maxPostponement)
if holdTime.After(limitTime) {
return limitTime, nil
}

return holdTime, nil
Expand Down
152 changes: 101 additions & 51 deletions overlord/snapstate/autorefresh_gating.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2021 Canonical Ltd
* Copyright (C) 2021-2022 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
Expand Down Expand Up @@ -35,6 +35,7 @@ import (
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/strutil"
)

var gateAutoRefreshHookName = "gate-auto-refresh"
Expand Down Expand Up @@ -143,6 +144,36 @@ func holdDurationLeft(now time.Time, lastRefresh, firstHeld time.Time, maxDurati
return d2
}

// HoldRefreshesBySystem is used to hold snaps by the sys admin (denoted by the
// "system" holding snap). HoldTime can be "forever" to denote an indefinite hold
// or any RFC3339 timestamp.
func HoldRefreshesBySystem(st *state.State, holdTime string, holdSnaps []string) error {
snaps, err := All(st)
if err != nil {
return err
}

for _, holdSnap := range holdSnaps {
if _, ok := snaps[holdSnap]; !ok {
return snap.NotInstalledError{Snap: holdSnap}
}
}

// zero value durations denote max allowed time in HoldRefresh
var holdDuration time.Duration
if holdTime != "forever" {
holdTime, err := time.Parse(time.RFC3339, holdTime)
if err != nil {
return err
}

holdDuration = holdTime.Sub(timeNow())
}

_, err = HoldRefresh(st, "system", holdDuration, holdSnaps...)
return err
}

// HoldRefresh marks affectingSnaps as held for refresh for up to holdTime.
// HoldTime of zero denotes maximum allowed hold time.
// Holding fails if not all snaps can be held, in that case HoldError is returned
Expand All @@ -162,56 +193,69 @@ func HoldRefresh(st *state.State, gatingSnap string, holdDuration time.Duration,

now := timeNow()
for _, heldSnap := range affectingSnaps {
var left time.Duration

hold, ok := gating[heldSnap][gatingSnap]
if !ok {
hold = &holdState{
FirstHeld: now,
}
}

lastRefreshTime, err := lastRefreshed(st, heldSnap)
if err != nil {
return 0, err
}

mp := maxPostponement - maxPostponementBuffer
maxDur := maxAllowedPostponement(gatingSnap, heldSnap, mp)

// calculate max hold duration that's left considering previous hold
// requests of this snap and last refresh time.
left := holdDurationLeft(now, lastRefreshTime, hold.FirstHeld, maxDur, mp)
if left <= 0 {
herr.SnapsInError[heldSnap] = HoldDurationError{
Err: fmt.Errorf("snap %q cannot hold snap %q anymore, maximum refresh postponement exceeded", gatingSnap, heldSnap),
if gatingSnap == "system" {
if holdDuration == 0 {
holdDuration = maxDuration
}
continue
}

dur := holdDuration
if dur == 0 {
// duration not specified, using a default one (maximum) or what's
// left of it.
dur = left
// if snap is being gated by "system" (it was set by the system admin), it
// can be held by any amount of time and no checks are required
hold.HoldUntil = now.Add(holdDuration)
left = holdDuration
} else {
// explicit hold duration requested
if dur > maxDur {
lastRefreshTime, err := lastRefreshed(st, heldSnap)
if err != nil {
return 0, err
}

mp := maxPostponement - maxPostponementBuffer
maxDur := maxAllowedPostponement(gatingSnap, heldSnap, mp)

// calculate max hold duration that's left considering previous hold
// requests of this snap and last refresh time.
left = holdDurationLeft(now, lastRefreshTime, hold.FirstHeld, maxDur, mp)
if left <= 0 {
herr.SnapsInError[heldSnap] = HoldDurationError{
Err: fmt.Errorf("requested holding duration for snap %q of %s by snap %q exceeds maximum holding time", heldSnap, holdDuration, gatingSnap),
DurationLeft: left,
Err: fmt.Errorf("snap %q cannot hold snap %q anymore, maximum refresh postponement exceeded", gatingSnap, heldSnap),
}
continue
}
}

newHold := now.Add(dur)
cutOff := lastRefreshTime.Add(maxPostponement - maxPostponementBuffer)
dur := holdDuration
if dur == 0 {
// duration not specified, using a default one (maximum) or what's
// left of it.
dur = left
} else {
// explicit hold duration requested
if dur > maxDur {
herr.SnapsInError[heldSnap] = HoldDurationError{
Err: fmt.Errorf("requested holding duration for snap %q of %s by snap %q exceeds maximum holding time", heldSnap, holdDuration, gatingSnap),
DurationLeft: left,
}
continue
}
}

newHold := now.Add(dur)
cutOff := lastRefreshTime.Add(maxPostponement - maxPostponementBuffer)

// consider last refresh time and adjust hold duration if needed so it's
// not exceeded.
if newHold.Before(cutOff) {
hold.HoldUntil = newHold
} else {
hold.HoldUntil = cutOff
// consider last refresh time and adjust hold duration if needed so it's
// not exceeded.
if newHold.Before(cutOff) {
hold.HoldUntil = newHold
} else {
hold.HoldUntil = cutOff
}
}

// finally store/update gating hold data
Expand Down Expand Up @@ -244,19 +288,21 @@ func HoldRefresh(st *state.State, gatingSnap string, holdDuration time.Duration,
return durationMin, nil
}

// ProceedWithRefresh unblocks all snaps held by gatingSnap for refresh. This
// ProceedWithRefresh unblocks a set of snaps held by gatingSnap for refresh.
// If no snaps are specified, all snaps held by gatingSnap are unblocked. This
// should be called for --proceed on the gatingSnap.
func ProceedWithRefresh(st *state.State, gatingSnap string) error {
func ProceedWithRefresh(st *state.State, gatingSnap string, unholdSnaps []string) error {
gating, err := refreshGating(st)
if err != nil {
return err
}
if len(gating) == 0 {
return nil
}

var changed bool
for heldSnap, gatingSnaps := range gating {
if len(unholdSnaps) != 0 && !strutil.ListContains(unholdSnaps, heldSnap) {
continue
}

if _, ok := gatingSnaps[gatingSnap]; ok {
delete(gatingSnaps, gatingSnap)
changed = true
Expand All @@ -269,6 +315,7 @@ func ProceedWithRefresh(st *state.State, gatingSnap string) error {
if changed {
st.Set("snaps-hold", gating)
}

return nil
}

Expand Down Expand Up @@ -312,7 +359,8 @@ func resetGatingForRefreshed(st *state.State, refreshedSnaps ...string) error {

var changed bool
for _, snapName := range refreshedSnaps {
if _, ok := gating[snapName]; ok {
// holds placed by the user remain after a refresh
if _, ok := gating[snapName]; ok && snapName != "system" {
delete(gating, snapName)
changed = true
}
Expand Down Expand Up @@ -360,8 +408,8 @@ func pruneSnapsHold(st *state.State, snapName string) error {
return nil
}

// heldSnaps returns all snaps that are gated and shouldn't be refreshed.
func heldSnaps(st *state.State) (map[string]bool, error) {
// HeldSnaps returns all snaps that are gated and shouldn't be refreshed.
func HeldSnaps(st *state.State) (map[string]bool, error) {
gating, err := refreshGating(st)
if err != nil {
return nil, err
Expand All @@ -374,16 +422,18 @@ func heldSnaps(st *state.State) (map[string]bool, error) {

held := make(map[string]bool)
Loop:
for heldSnap, holdingSnaps := range gating {
refreshed, err := lastRefreshed(st, heldSnap)
for heldSnap, holds := range gating {
lastRefresh, err := lastRefreshed(st, heldSnap)
if err != nil {
return nil, err
}
// make sure we don't hold any snap for more than maxPostponement
if refreshed.Add(maxPostponement).Before(now) {
continue
}
for _, hold := range holdingSnaps {

for holdingSnap, hold := range holds {
// enforce the maxPostponement limit on a hold, unless it's held by the user
if holdingSnap != "system" && lastRefresh.Add(maxPostponement).Before(now) {
continue
}

if hold.HoldUntil.Before(now) {
continue
}
Expand Down Expand Up @@ -628,7 +678,7 @@ var snapsToRefresh = func(gatingTask *state.Task) ([]*refreshCandidate, error) {
return nil, err
}

held, err := heldSnaps(gatingTask.State())
held, err := HeldSnaps(gatingTask.State())
if err != nil {
return nil, err
}
Expand Down

0 comments on commit 9bb978e

Please sign in to comment.