github.com/rigado/snapd@v2.42.5-go-mod+incompatible/overlord/snapstate/autorefresh.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 "os" 25 "time" 26 27 "github.com/snapcore/snapd/httputil" 28 "github.com/snapcore/snapd/i18n" 29 "github.com/snapcore/snapd/logger" 30 "github.com/snapcore/snapd/overlord/auth" 31 "github.com/snapcore/snapd/overlord/configstate/config" 32 "github.com/snapcore/snapd/overlord/state" 33 "github.com/snapcore/snapd/release" 34 "github.com/snapcore/snapd/snap" 35 "github.com/snapcore/snapd/strutil" 36 "github.com/snapcore/snapd/timeutil" 37 "github.com/snapcore/snapd/timings" 38 ) 39 40 // the default refresh pattern 41 const defaultRefreshSchedule = "00:00~24:00/4" 42 43 // cannot keep without refreshing for more than maxPostponement 44 const maxPostponement = 60 * 24 * time.Hour 45 46 // cannot inhibit refreshes for more than maxInhibition 47 const maxInhibition = 7 * 24 * time.Hour 48 49 // hooks setup by devicestate 50 var ( 51 CanAutoRefresh func(st *state.State) (bool, error) 52 CanManageRefreshes func(st *state.State) bool 53 IsOnMeteredConnection func() (bool, error) 54 ) 55 56 // refreshRetryDelay specified the minimum time to retry failed refreshes 57 var refreshRetryDelay = 20 * time.Minute 58 59 // autoRefresh will ensure that snaps are refreshed automatically 60 // according to the refresh schedule. 61 type autoRefresh struct { 62 state *state.State 63 64 lastRefreshSchedule string 65 nextRefresh time.Time 66 lastRefreshAttempt time.Time 67 } 68 69 func newAutoRefresh(st *state.State) *autoRefresh { 70 return &autoRefresh{ 71 state: st, 72 } 73 } 74 75 // RefreshSchedule will return a user visible string with the current schedule 76 // for the automatic refreshes and a flag indicating whether the schedule is a 77 // legacy one. 78 func (m *autoRefresh) RefreshSchedule() (schedule string, legacy bool, err error) { 79 _, schedule, legacy, err = m.refreshScheduleWithDefaultsFallback() 80 return schedule, legacy, err 81 } 82 83 // NextRefresh returns when the next automatic refresh will happen. 84 func (m *autoRefresh) NextRefresh() time.Time { 85 return m.nextRefresh 86 } 87 88 // LastRefresh returns when the last refresh happened. 89 func (m *autoRefresh) LastRefresh() (time.Time, error) { 90 return getTime(m.state, "last-refresh") 91 } 92 93 // EffectiveRefreshHold returns the time until to which refreshes are 94 // held if refresh.hold configuration is set and accounting for the 95 // max postponement since the last refresh. 96 func (m *autoRefresh) EffectiveRefreshHold() (time.Time, error) { 97 var holdTime time.Time 98 99 tr := config.NewTransaction(m.state) 100 err := tr.Get("core", "refresh.hold", &holdTime) 101 if err != nil && !config.IsNoOption(err) { 102 return time.Time{}, err 103 } 104 105 // cannot hold beyond last-refresh + max-postponement 106 lastRefresh, err := m.LastRefresh() 107 if err != nil { 108 return time.Time{}, err 109 } 110 if lastRefresh.IsZero() { 111 seedTime, err := getTime(m.state, "seed-time") 112 if err != nil { 113 return time.Time{}, err 114 } 115 if seedTime.IsZero() { 116 // no reference to know whether holding is reasonable 117 return time.Time{}, nil 118 } 119 lastRefresh = seedTime 120 } 121 122 limitTime := lastRefresh.Add(maxPostponement) 123 if holdTime.After(limitTime) { 124 return limitTime, nil 125 } 126 127 return holdTime, nil 128 } 129 130 // clearRefreshHold clears refresh.hold configuration. 131 func (m *autoRefresh) clearRefreshHold() { 132 tr := config.NewTransaction(m.state) 133 tr.Set("core", "refresh.hold", nil) 134 tr.Commit() 135 } 136 137 // AtSeed configures refresh policies at end of seeding. 138 func (m *autoRefresh) AtSeed() error { 139 // on classic hold refreshes for 2h after seeding 140 if release.OnClassic { 141 var t1 time.Time 142 tr := config.NewTransaction(m.state) 143 err := tr.Get("core", "refresh.hold", &t1) 144 if !config.IsNoOption(err) { 145 // already set or error 146 return err 147 } 148 // TODO: have a policy that if the snapd exe itself 149 // is older than X weeks/months we skip the holding? 150 now := time.Now().UTC() 151 tr.Set("core", "refresh.hold", now.Add(2*time.Hour)) 152 tr.Commit() 153 m.nextRefresh = now 154 } 155 return nil 156 } 157 158 func canRefreshOnMeteredConnection(st *state.State) (bool, error) { 159 tr := config.NewTransaction(st) 160 var onMetered string 161 err := tr.GetMaybe("core", "refresh.metered", &onMetered) 162 if err != nil && err != state.ErrNoState { 163 return false, err 164 } 165 166 return onMetered != "hold", nil 167 } 168 169 func (m *autoRefresh) canRefreshRespectingMetered(now, lastRefresh time.Time) (can bool, err error) { 170 can, err = canRefreshOnMeteredConnection(m.state) 171 if err != nil { 172 return false, err 173 } 174 if can { 175 return true, nil 176 } 177 178 // ignore any errors that occurred while checking if we are on a metered 179 // connection 180 metered, _ := IsOnMeteredConnection() 181 if !metered { 182 return true, nil 183 } 184 185 if now.Sub(lastRefresh) >= maxPostponement { 186 // TODO use warnings when the infra becomes available 187 logger.Noticef("Auto refresh disabled while on metered connections, but pending for too long (%d days). Trying to refresh now.", int(maxPostponement.Hours()/24)) 188 return true, nil 189 } 190 191 logger.Debugf("Auto refresh disabled on metered connections") 192 193 return false, nil 194 } 195 196 // Ensure ensures that we refresh all installed snaps periodically 197 func (m *autoRefresh) Ensure() error { 198 m.state.Lock() 199 defer m.state.Unlock() 200 201 // see if it even makes sense to try to refresh 202 if CanAutoRefresh == nil { 203 return nil 204 } 205 if ok, err := CanAutoRefresh(m.state); err != nil || !ok { 206 return err 207 } 208 209 // get lastRefresh and schedule 210 lastRefresh, err := m.LastRefresh() 211 if err != nil { 212 return err 213 } 214 215 refreshSchedule, refreshScheduleStr, _, err := m.refreshScheduleWithDefaultsFallback() 216 if err != nil { 217 return err 218 } 219 if len(refreshSchedule) == 0 { 220 m.nextRefresh = time.Time{} 221 return nil 222 } 223 // we already have a refresh time, check if we got a new config 224 if !m.nextRefresh.IsZero() { 225 if m.lastRefreshSchedule != refreshScheduleStr { 226 // the refresh schedule has changed 227 logger.Debugf("Refresh timer changed.") 228 m.nextRefresh = time.Time{} 229 } 230 } 231 m.lastRefreshSchedule = refreshScheduleStr 232 233 // ensure nothing is in flight already 234 if autoRefreshInFlight(m.state) { 235 return nil 236 } 237 238 now := time.Now() 239 // compute next refresh attempt time (if needed) 240 if m.nextRefresh.IsZero() { 241 // store attempts in memory so that we can backoff 242 if !lastRefresh.IsZero() { 243 delta := timeutil.Next(refreshSchedule, lastRefresh, maxPostponement) 244 now = time.Now() 245 m.nextRefresh = now.Add(delta) 246 } else { 247 // make sure either seed-time or last-refresh 248 // are set for hold code below 249 m.ensureLastRefreshAnchor() 250 // immediate 251 m.nextRefresh = now 252 } 253 logger.Debugf("Next refresh scheduled for %s.", m.nextRefresh.Format(time.RFC3339)) 254 } 255 256 // should we hold back refreshes? 257 holdTime, err := m.EffectiveRefreshHold() 258 if err != nil { 259 return err 260 } 261 if holdTime.After(now) { 262 return nil 263 } 264 if !holdTime.IsZero() { 265 // expired hold case 266 m.clearRefreshHold() 267 if m.nextRefresh.Before(holdTime) { 268 // next refresh is obsolete, compute the next one 269 delta := timeutil.Next(refreshSchedule, holdTime, maxPostponement) 270 now = time.Now() 271 m.nextRefresh = now.Add(delta) 272 } 273 } 274 275 // do refresh attempt (if needed) 276 if !m.nextRefresh.After(now) { 277 var can bool 278 can, err = m.canRefreshRespectingMetered(now, lastRefresh) 279 if err != nil { 280 return err 281 } 282 if !can { 283 // clear nextRefresh so that another refresh time is calculated 284 m.nextRefresh = time.Time{} 285 return nil 286 } 287 288 // Check that we have reasonable delays between attempts. 289 // If the store is under stress we need to make sure we do not 290 // hammer it too often 291 if !m.lastRefreshAttempt.IsZero() && m.lastRefreshAttempt.Add(refreshRetryDelay).After(time.Now()) { 292 return nil 293 } 294 295 err = m.launchAutoRefresh() 296 if _, ok := err.(*httputil.PerstistentNetworkError); !ok { 297 m.nextRefresh = time.Time{} 298 } // else - refresh will be retried after refreshRetryDelay 299 } 300 301 return err 302 } 303 304 func (m *autoRefresh) ensureLastRefreshAnchor() { 305 seedTime, _ := getTime(m.state, "seed-time") 306 if !seedTime.IsZero() { 307 return 308 } 309 310 // last core refresh 311 coreRefreshDate := snap.InstallDate("core") 312 if !coreRefreshDate.IsZero() { 313 m.state.Set("last-refresh", coreRefreshDate) 314 return 315 } 316 317 // fallback to executable time 318 st, err := os.Stat("/proc/self/exe") 319 if err == nil { 320 m.state.Set("last-refresh", st.ModTime()) 321 return 322 } 323 } 324 325 // refreshScheduleWithDefaultsFallback returns the current refresh schedule 326 // and refresh string. When an invalid refresh schedule is set by the user 327 // the refresh schedule is automatically reset to the default. 328 // 329 // TODO: we can remove the refreshSchedule reset because we have validation 330 // of the schedule now. 331 func (m *autoRefresh) refreshScheduleWithDefaultsFallback() (ts []*timeutil.Schedule, scheduleAsStr string, legacy bool, err error) { 332 if managed, legacy := refreshScheduleManaged(m.state); managed { 333 if m.lastRefreshSchedule != "managed" { 334 logger.Noticef("refresh is managed via the snapd-control interface") 335 m.lastRefreshSchedule = "managed" 336 } 337 return nil, "managed", legacy, nil 338 } 339 340 tr := config.NewTransaction(m.state) 341 // try the new refresh.timer config option first 342 err = tr.Get("core", "refresh.timer", &scheduleAsStr) 343 if err != nil && !config.IsNoOption(err) { 344 return nil, "", false, err 345 } 346 if scheduleAsStr != "" { 347 ts, err = timeutil.ParseSchedule(scheduleAsStr) 348 if err != nil { 349 logger.Noticef("cannot use refresh.timer configuration: %s", err) 350 return refreshScheduleDefault() 351 } 352 return ts, scheduleAsStr, false, nil 353 } 354 355 // fallback to legacy refresh.schedule setting when the new 356 // config option is not set 357 err = tr.Get("core", "refresh.schedule", &scheduleAsStr) 358 if err != nil && !config.IsNoOption(err) { 359 return nil, "", false, err 360 } 361 if scheduleAsStr != "" { 362 ts, err = timeutil.ParseLegacySchedule(scheduleAsStr) 363 if err != nil { 364 logger.Noticef("cannot use refresh.schedule configuration: %s", err) 365 return refreshScheduleDefault() 366 } 367 return ts, scheduleAsStr, true, nil 368 } 369 370 return refreshScheduleDefault() 371 } 372 373 // launchAutoRefresh creates the auto-refresh taskset and a change for it. 374 func (m *autoRefresh) launchAutoRefresh() error { 375 perfTimings := timings.New(map[string]string{"ensure": "auto-refresh"}) 376 tm := perfTimings.StartSpan("auto-refresh", "query store and setup auto-refresh change") 377 defer func() { 378 tm.Stop() 379 perfTimings.Save(m.state) 380 }() 381 382 m.lastRefreshAttempt = time.Now() 383 updated, tasksets, err := AutoRefresh(auth.EnsureContextTODO(), m.state) 384 if _, ok := err.(*httputil.PerstistentNetworkError); ok { 385 logger.Noticef("Cannot prepare auto-refresh change due to a permanent network error: %s", err) 386 return err 387 } 388 m.state.Set("last-refresh", time.Now()) 389 if err != nil { 390 logger.Noticef("Cannot prepare auto-refresh change: %s", err) 391 return err 392 } 393 394 var msg string 395 switch len(updated) { 396 case 0: 397 logger.Noticef(i18n.G("auto-refresh: all snaps are up-to-date")) 398 return nil 399 case 1: 400 msg = fmt.Sprintf(i18n.G("Auto-refresh snap %q"), updated[0]) 401 case 2, 3: 402 quoted := strutil.Quoted(updated) 403 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 404 msg = fmt.Sprintf(i18n.G("Auto-refresh snaps %s"), quoted) 405 default: 406 msg = fmt.Sprintf(i18n.G("Auto-refresh %d snaps"), len(updated)) 407 } 408 409 chg := m.state.NewChange("auto-refresh", msg) 410 for _, ts := range tasksets { 411 chg.AddAll(ts) 412 } 413 chg.Set("snap-names", updated) 414 chg.Set("api-data", map[string]interface{}{"snap-names": updated}) 415 perfTimings.AddTag("change-id", chg.ID()) 416 417 return nil 418 } 419 420 func refreshScheduleDefault() (ts []*timeutil.Schedule, scheduleStr string, legacy bool, err error) { 421 refreshSchedule, err := timeutil.ParseSchedule(defaultRefreshSchedule) 422 if err != nil { 423 panic(fmt.Sprintf("defaultRefreshSchedule cannot be parsed: %s", err)) 424 } 425 426 return refreshSchedule, defaultRefreshSchedule, false, nil 427 } 428 429 func autoRefreshInFlight(st *state.State) bool { 430 for _, chg := range st.Changes() { 431 if chg.Kind() == "auto-refresh" && !chg.Status().Ready() { 432 return true 433 } 434 } 435 return false 436 } 437 438 // refreshScheduleManaged returns true if the refresh schedule of the 439 // device is managed by an external snap 440 func refreshScheduleManaged(st *state.State) (managed bool, legacy bool) { 441 var confStr string 442 443 // this will only be "nil" if running in tests 444 if CanManageRefreshes == nil { 445 return false, legacy 446 } 447 448 // check new style timer first 449 tr := config.NewTransaction(st) 450 err := tr.Get("core", "refresh.timer", &confStr) 451 if err != nil && !config.IsNoOption(err) { 452 return false, legacy 453 } 454 // if not set, fallback to refresh.schedule 455 if confStr == "" { 456 if err := tr.Get("core", "refresh.schedule", &confStr); err != nil { 457 return false, legacy 458 } 459 legacy = true 460 } 461 462 if confStr != "managed" { 463 return false, legacy 464 } 465 return CanManageRefreshes(st), legacy 466 } 467 468 // getTime retrieves a time from a state value. 469 func getTime(st *state.State, timeKey string) (time.Time, error) { 470 var t1 time.Time 471 err := st.Get(timeKey, &t1) 472 if err != nil && err != state.ErrNoState { 473 return time.Time{}, err 474 } 475 return t1, nil 476 } 477 478 // inhibitRefresh returns an error if refresh is inhibited by running apps. 479 // 480 // Internally the snap state is updated to remember when the inhibition first 481 // took place. Apps can inhibit refreshes for up to "maxInhibition", beyond 482 // that period the refresh will go ahead despite application activity. 483 func inhibitRefresh(st *state.State, snapst *SnapState, info *snap.Info, checker func(*snap.Info) error) error { 484 if err := checker(info); err != nil { 485 now := time.Now() 486 if snapst.RefreshInhibitedTime == nil { 487 // Store the instant when the snap was first inhibited. 488 // This is reset to nil on successful refresh. 489 snapst.RefreshInhibitedTime = &now 490 Set(st, info.InstanceName(), snapst) 491 return err 492 } 493 494 if now.Sub(*snapst.RefreshInhibitedTime) < maxInhibition { 495 // If we are still in the allowed window then just return 496 // the error but don't change the snap state again. 497 return err 498 } 499 } 500 return nil 501 }