github.com/tompreston/snapd@v0.0.0-20210817193607-954edfcb9611/overlord/snapstate/refreshhints.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2017 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package snapstate 21 22 import ( 23 "fmt" 24 "time" 25 26 "github.com/snapcore/snapd/logger" 27 "github.com/snapcore/snapd/overlord/auth" 28 "github.com/snapcore/snapd/overlord/state" 29 "github.com/snapcore/snapd/release" 30 "github.com/snapcore/snapd/snap" 31 "github.com/snapcore/snapd/store" 32 "github.com/snapcore/snapd/timings" 33 ) 34 35 var refreshHintsDelay = time.Duration(24 * time.Hour) 36 37 // refreshHints will ensure that we get regular data about refreshes 38 // so that we can potentially warn the user about important missing 39 // refreshes. 40 type refreshHints struct { 41 state *state.State 42 } 43 44 func newRefreshHints(st *state.State) *refreshHints { 45 return &refreshHints{state: st} 46 } 47 48 func (r *refreshHints) lastRefresh(timestampKey string) (time.Time, error) { 49 return getTime(r.state, timestampKey) 50 } 51 52 func (r *refreshHints) needsUpdate() (bool, error) { 53 tFull, err := r.lastRefresh("last-refresh") 54 if err != nil { 55 return false, err 56 } 57 tHints, err := r.lastRefresh("last-refresh-hints") 58 if err != nil { 59 return false, err 60 } 61 62 recentEnough := time.Now().Add(-refreshHintsDelay) 63 if tFull.After(recentEnough) || tFull.Equal(recentEnough) { 64 return false, nil 65 } 66 return tHints.Before(recentEnough), nil 67 } 68 69 func (r *refreshHints) refresh() error { 70 var refreshManaged bool 71 refreshManaged, _, _ = refreshScheduleManaged(r.state) 72 73 var err error 74 perfTimings := timings.New(map[string]string{"ensure": "refresh-hints"}) 75 defer perfTimings.Save(r.state) 76 77 var updates []*snap.Info 78 var ignoreValidationByInstanceName map[string]bool 79 timings.Run(perfTimings, "refresh-candidates", "query store for refresh candidates", func(tm timings.Measurer) { 80 updates, _, ignoreValidationByInstanceName, err = refreshCandidates(auth.EnsureContextTODO(), r.state, nil, nil, &store.RefreshOptions{RefreshManaged: refreshManaged}) 81 }) 82 // TODO: we currently set last-refresh-hints even when there was an 83 // error. In the future we may retry with a backoff. 84 r.state.Set("last-refresh-hints", time.Now()) 85 86 if err != nil { 87 return err 88 } 89 deviceCtx, err := DeviceCtxFromState(r.state, nil) 90 if err != nil { 91 return err 92 } 93 hints, err := refreshHintsFromCandidates(r.state, updates, ignoreValidationByInstanceName, deviceCtx) 94 if err != nil { 95 return fmt.Errorf("internal error: cannot get refresh-candidates: %v", err) 96 } 97 r.state.Set("refresh-candidates", hints) 98 return nil 99 } 100 101 // AtSeed configures hints refresh policies at end of seeding. 102 func (r *refreshHints) AtSeed() error { 103 // on classic hold hints refreshes for a full 24h 104 if release.OnClassic { 105 var t1 time.Time 106 err := r.state.Get("last-refresh-hints", &t1) 107 if err != state.ErrNoState { 108 // already set or other error 109 return err 110 } 111 r.state.Set("last-refresh-hints", time.Now()) 112 } 113 return nil 114 } 115 116 // Ensure will ensure that refresh hints are available on a regular 117 // interval. 118 func (r *refreshHints) Ensure() error { 119 r.state.Lock() 120 defer r.state.Unlock() 121 122 // CanAutoRefresh is a hook that is set by the devicestate 123 // code to ensure that we only AutoRefersh if the device has 124 // bootstraped itself enough. This is only nil when snapstate 125 // is used in isolation (like in tests). 126 if CanAutoRefresh == nil { 127 return nil 128 } 129 if ok, err := CanAutoRefresh(r.state); err != nil || !ok { 130 return err 131 } 132 133 needsUpdate, err := r.needsUpdate() 134 if err != nil { 135 return err 136 } 137 if !needsUpdate { 138 return nil 139 } 140 return r.refresh() 141 } 142 143 func refreshHintsFromCandidates(st *state.State, updates []*snap.Info, ignoreValidationByInstanceName map[string]bool, deviceCtx DeviceContext) (map[string]*refreshCandidate, error) { 144 if ValidateRefreshes != nil && len(updates) != 0 { 145 userID := 0 146 var err error 147 updates, err = ValidateRefreshes(st, updates, ignoreValidationByInstanceName, userID, deviceCtx) 148 if err != nil { 149 return nil, err 150 } 151 } 152 153 hints := make(map[string]*refreshCandidate, len(updates)) 154 for _, update := range updates { 155 var snapst SnapState 156 if err := Get(st, update.InstanceName(), &snapst); err != nil { 157 return nil, err 158 } 159 160 flags := snapst.Flags 161 flags.IsAutoRefresh = true 162 flags, err := earlyChecks(st, &snapst, update, flags) 163 if err != nil { 164 logger.Debugf("update hint for %q is not applicable: %v", update.InstanceName(), err) 165 continue 166 } 167 168 snapsup := &refreshCandidate{ 169 SnapSetup: SnapSetup{ 170 Base: update.Base, 171 Prereq: defaultContentPlugProviders(st, update), 172 Channel: snapst.TrackingChannel, 173 CohortKey: snapst.CohortKey, 174 // UserID not set 175 Flags: flags.ForSnapSetup(), 176 DownloadInfo: &update.DownloadInfo, 177 SideInfo: &update.SideInfo, 178 Type: update.Type(), 179 PlugsOnly: len(update.Slots) == 0, 180 InstanceKey: update.InstanceKey, 181 auxStoreInfo: auxStoreInfo{ 182 Website: update.Website, 183 Media: update.Media, 184 }, 185 }, 186 Version: update.Version, 187 } 188 hints[update.InstanceName()] = snapsup 189 } 190 return hints, nil 191 } 192 193 // pruneRefreshCandidates removes the given snaps from refresh-candidates map 194 // in the state. 195 func pruneRefreshCandidates(st *state.State, snaps ...string) error { 196 var candidates map[string]*refreshCandidate 197 198 err := st.Get("refresh-candidates", &candidates) 199 if err != nil { 200 if err == state.ErrNoState { 201 return nil 202 } 203 return err 204 } 205 206 for _, snapName := range snaps { 207 delete(candidates, snapName) 208 } 209 210 st.Set("refresh-candidates", candidates) 211 return nil 212 }