github.com/rigado/snapd@v2.42.5-go-mod+incompatible/overlord/snapstate/autorefresh_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2017-2018 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_test 21 22 import ( 23 "context" 24 "fmt" 25 "os" 26 "path/filepath" 27 "time" 28 29 . "gopkg.in/check.v1" 30 31 "github.com/snapcore/snapd/dirs" 32 "github.com/snapcore/snapd/httputil" 33 "github.com/snapcore/snapd/overlord/auth" 34 "github.com/snapcore/snapd/overlord/configstate/config" 35 "github.com/snapcore/snapd/overlord/snapstate" 36 "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" 37 "github.com/snapcore/snapd/overlord/state" 38 "github.com/snapcore/snapd/release" 39 "github.com/snapcore/snapd/snap" 40 "github.com/snapcore/snapd/store" 41 "github.com/snapcore/snapd/store/storetest" 42 "github.com/snapcore/snapd/timeutil" 43 ) 44 45 type autoRefreshStore struct { 46 storetest.Store 47 48 ops []string 49 50 err error 51 } 52 53 func (r *autoRefreshStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error) { 54 if !opts.IsAutoRefresh { 55 panic("AutoRefresh snap action did not set IsAutoRefresh flag") 56 } 57 58 if ctx == nil || !auth.IsEnsureContext(ctx) { 59 panic("Ensure marked context required") 60 } 61 if len(currentSnaps) != len(actions) || len(currentSnaps) == 0 { 62 panic("expected in test one action for each current snaps, and at least one snap") 63 } 64 for _, a := range actions { 65 if a.Action != "refresh" { 66 panic("expected refresh actions") 67 } 68 } 69 r.ops = append(r.ops, "list-refresh") 70 return nil, r.err 71 } 72 73 type autoRefreshTestSuite struct { 74 state *state.State 75 76 store *autoRefreshStore 77 78 restore func() 79 } 80 81 var _ = Suite(&autoRefreshTestSuite{}) 82 83 func (s *autoRefreshTestSuite) SetUpTest(c *C) { 84 dirs.SetRootDir(c.MkDir()) 85 86 s.state = state.New(nil) 87 88 s.store = &autoRefreshStore{} 89 90 s.state.Lock() 91 defer s.state.Unlock() 92 snapstate.ReplaceStore(s.state, s.store) 93 94 snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ 95 Active: true, 96 Sequence: []*snap.SideInfo{ 97 {RealName: "some-snap", Revision: snap.R(5), SnapID: "some-snap-id"}, 98 }, 99 Current: snap.R(5), 100 SnapType: "app", 101 UserID: 1, 102 }) 103 104 snapstate.CanAutoRefresh = func(*state.State) (bool, error) { return true, nil } 105 snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) { 106 return nil, nil 107 } 108 snapstate.IsOnMeteredConnection = func() (bool, error) { return false, nil } 109 110 s.state.Set("seeded", true) 111 s.state.Set("seed-time", time.Now()) 112 s.state.Set("refresh-privacy-key", "privacy-key") 113 s.restore = snapstatetest.MockDeviceModel(DefaultModel()) 114 } 115 116 func (s *autoRefreshTestSuite) TearDownTest(c *C) { 117 snapstate.CanAutoRefresh = nil 118 snapstate.AutoAliases = nil 119 s.restore() 120 dirs.SetRootDir("") 121 } 122 123 func (s *autoRefreshTestSuite) TestLastRefresh(c *C) { 124 // this does an immediate refresh 125 126 af := snapstate.NewAutoRefresh(s.state) 127 err := af.Ensure() 128 c.Check(err, IsNil) 129 c.Check(s.store.ops, DeepEquals, []string{"list-refresh"}) 130 131 var lastRefresh time.Time 132 s.state.Lock() 133 s.state.Get("last-refresh", &lastRefresh) 134 s.state.Unlock() 135 c.Check(lastRefresh.Year(), Equals, time.Now().Year()) 136 } 137 138 func (s *autoRefreshTestSuite) TestLastRefreshRefreshManaged(c *C) { 139 snapstate.CanManageRefreshes = func(st *state.State) bool { 140 return true 141 } 142 defer func() { snapstate.CanManageRefreshes = nil }() 143 144 s.state.Lock() 145 defer s.state.Unlock() 146 147 for _, t := range []struct { 148 conf string 149 legacy bool 150 }{ 151 {"refresh.timer", false}, 152 {"refresh.schedule", true}, 153 } { 154 tr := config.NewTransaction(s.state) 155 tr.Set("core", t.conf, "managed") 156 tr.Commit() 157 158 af := snapstate.NewAutoRefresh(s.state) 159 s.state.Unlock() 160 err := af.Ensure() 161 s.state.Lock() 162 c.Check(err, IsNil) 163 c.Check(s.store.ops, HasLen, 0) 164 165 refreshScheduleStr, legacy, err := af.RefreshSchedule() 166 c.Check(refreshScheduleStr, Equals, "managed") 167 c.Check(legacy, Equals, t.legacy) 168 c.Check(err, IsNil) 169 170 c.Check(af.NextRefresh(), DeepEquals, time.Time{}) 171 172 // ensure clean config for the next run 173 s.state.Set("config", nil) 174 } 175 } 176 177 func (s *autoRefreshTestSuite) TestRefreshManagedTimerWins(c *C) { 178 snapstate.CanManageRefreshes = func(st *state.State) bool { 179 return true 180 } 181 defer func() { snapstate.CanManageRefreshes = nil }() 182 183 s.state.Lock() 184 defer s.state.Unlock() 185 186 tr := config.NewTransaction(s.state) 187 // the "refresh.timer" setting always takes precedence over 188 // refresh.schedule 189 tr.Set("core", "refresh.timer", "00:00-12:00") 190 tr.Set("core", "refresh.schedule", "managed") 191 tr.Commit() 192 193 af := snapstate.NewAutoRefresh(s.state) 194 s.state.Unlock() 195 err := af.Ensure() 196 s.state.Lock() 197 c.Check(err, IsNil) 198 c.Check(s.store.ops, DeepEquals, []string{"list-refresh"}) 199 200 refreshScheduleStr, legacy, err := af.RefreshSchedule() 201 c.Check(refreshScheduleStr, Equals, "00:00-12:00") 202 c.Check(legacy, Equals, false) 203 c.Check(err, IsNil) 204 } 205 206 func (s *autoRefreshTestSuite) TestLastRefreshNoRefreshNeeded(c *C) { 207 s.state.Lock() 208 s.state.Set("last-refresh", time.Now()) 209 s.state.Unlock() 210 211 af := snapstate.NewAutoRefresh(s.state) 212 err := af.Ensure() 213 c.Check(err, IsNil) 214 c.Check(s.store.ops, HasLen, 0) 215 } 216 217 func (s *autoRefreshTestSuite) TestRefreshBackoff(c *C) { 218 s.store.err = fmt.Errorf("random store error") 219 af := snapstate.NewAutoRefresh(s.state) 220 err := af.Ensure() 221 c.Check(err, ErrorMatches, "random store error") 222 c.Check(s.store.ops, HasLen, 1) 223 224 // override next refresh to be here already 225 now := time.Now() 226 snapstate.MockNextRefresh(af, now) 227 228 // call ensure again, our back-off will prevent the store from 229 // being hit again 230 err = af.Ensure() 231 c.Check(err, IsNil) 232 c.Check(s.store.ops, HasLen, 1) 233 234 // nextRefresh unchanged 235 c.Check(af.NextRefresh().Equal(now), Equals, true) 236 237 // fake that the retryRefreshDelay is over 238 restore := snapstate.MockRefreshRetryDelay(1 * time.Millisecond) 239 defer restore() 240 time.Sleep(10 * time.Millisecond) 241 242 // ensure hits the store again 243 err = af.Ensure() 244 c.Check(err, ErrorMatches, "random store error") 245 c.Check(s.store.ops, HasLen, 2) 246 247 // nextRefresh now zero 248 c.Check(af.NextRefresh().IsZero(), Equals, true) 249 // set it to something in the future 250 snapstate.MockNextRefresh(af, time.Now().Add(time.Minute)) 251 252 // nothing really happens yet: the previous autorefresh failed 253 // but it still counts as having tried to autorefresh 254 err = af.Ensure() 255 c.Check(err, IsNil) 256 c.Check(s.store.ops, HasLen, 2) 257 258 // pretend the time for next refresh is here 259 snapstate.MockNextRefresh(af, time.Now()) 260 // including the wait for the retryRefreshDelay backoff 261 time.Sleep(10 * time.Millisecond) 262 263 // now yes it happens again 264 err = af.Ensure() 265 c.Check(err, ErrorMatches, "random store error") 266 c.Check(s.store.ops, HasLen, 3) 267 // and not *again* again 268 err = af.Ensure() 269 c.Check(err, IsNil) 270 c.Check(s.store.ops, HasLen, 3) 271 272 c.Check(s.store.ops, DeepEquals, []string{"list-refresh", "list-refresh", "list-refresh"}) 273 } 274 275 func (s *autoRefreshTestSuite) TestRefreshPersistentError(c *C) { 276 // fake that the retryRefreshDelay is over 277 restore := snapstate.MockRefreshRetryDelay(1 * time.Millisecond) 278 defer restore() 279 280 initialLastRefresh := time.Now().Add(-12 * time.Hour) 281 s.state.Lock() 282 s.state.Set("last-refresh", initialLastRefresh) 283 s.state.Unlock() 284 285 s.store.err = &httputil.PerstistentNetworkError{Err: fmt.Errorf("error")} 286 af := snapstate.NewAutoRefresh(s.state) 287 err := af.Ensure() 288 c.Check(err, ErrorMatches, "persistent network error: error") 289 c.Check(s.store.ops, HasLen, 1) 290 291 // last-refresh time remains untouched 292 var lastRefresh time.Time 293 s.state.Lock() 294 s.state.Get("last-refresh", &lastRefresh) 295 s.state.Unlock() 296 c.Check(lastRefresh.Format(time.RFC3339), Equals, initialLastRefresh.Format(time.RFC3339)) 297 298 s.store.err = nil 299 time.Sleep(10 * time.Millisecond) 300 301 // call ensure again, refresh should be attempted again 302 err = af.Ensure() 303 c.Check(err, IsNil) 304 c.Check(s.store.ops, HasLen, 2) 305 } 306 307 func (s *autoRefreshTestSuite) TestDefaultScheduleIsRandomized(c *C) { 308 schedule, err := timeutil.ParseSchedule(snapstate.DefaultRefreshSchedule) 309 c.Assert(err, IsNil) 310 311 for _, sched := range schedule { 312 for _, span := range sched.ClockSpans { 313 c.Check(span.Start == span.End, Equals, false, 314 Commentf("clock span %v is a single time, expected an actual span", span)) 315 c.Check(span.Spread, Equals, true, 316 Commentf("clock span %v is not randomized", span)) 317 } 318 } 319 } 320 321 func (s *autoRefreshTestSuite) TestLastRefreshRefreshHold(c *C) { 322 s.state.Lock() 323 defer s.state.Unlock() 324 325 t0 := time.Now() 326 s.state.Set("last-refresh", t0.Add(-12*time.Hour)) 327 328 holdTime := t0.Add(5 * time.Minute) 329 tr := config.NewTransaction(s.state) 330 tr.Set("core", "refresh.hold", holdTime) 331 tr.Commit() 332 333 af := snapstate.NewAutoRefresh(s.state) 334 s.state.Unlock() 335 err := af.Ensure() 336 s.state.Lock() 337 c.Check(err, IsNil) 338 339 // no refresh 340 c.Check(s.store.ops, HasLen, 0) 341 342 // hold still kept 343 tr = config.NewTransaction(s.state) 344 var t1 time.Time 345 err = tr.Get("core", "refresh.hold", &t1) 346 c.Assert(err, IsNil) 347 c.Check(t1.Equal(holdTime), Equals, true) 348 } 349 350 func (s *autoRefreshTestSuite) TestLastRefreshRefreshHoldExpired(c *C) { 351 s.state.Lock() 352 defer s.state.Unlock() 353 354 t0 := time.Now() 355 s.state.Set("last-refresh", t0.Add(-12*time.Hour)) 356 357 holdTime := t0.Add(-5 * time.Minute) 358 tr := config.NewTransaction(s.state) 359 tr.Set("core", "refresh.hold", holdTime) 360 tr.Commit() 361 362 af := snapstate.NewAutoRefresh(s.state) 363 s.state.Unlock() 364 err := af.Ensure() 365 s.state.Lock() 366 c.Check(err, IsNil) 367 368 // refresh happened 369 c.Check(s.store.ops, DeepEquals, []string{"list-refresh"}) 370 371 var lastRefresh time.Time 372 s.state.Get("last-refresh", &lastRefresh) 373 c.Check(lastRefresh.Year(), Equals, time.Now().Year()) 374 375 // hold was reset 376 tr = config.NewTransaction(s.state) 377 var t1 time.Time 378 err = tr.Get("core", "refresh.hold", &t1) 379 c.Assert(config.IsNoOption(err), Equals, true) 380 } 381 382 func (s *autoRefreshTestSuite) TestLastRefreshRefreshHoldExpiredReschedule(c *C) { 383 s.state.Lock() 384 defer s.state.Unlock() 385 386 t0 := time.Now() 387 s.state.Set("last-refresh", t0.Add(-12*time.Hour)) 388 389 holdTime := t0.Add(-1 * time.Minute) 390 tr := config.NewTransaction(s.state) 391 tr.Set("core", "refresh.hold", holdTime) 392 393 nextRefresh := t0.Add(5 * time.Minute).Truncate(time.Minute) 394 schedule := fmt.Sprintf("%02d:%02d-%02d:59", nextRefresh.Hour(), nextRefresh.Minute(), nextRefresh.Hour()) 395 tr.Set("core", "refresh.timer", schedule) 396 tr.Commit() 397 398 af := snapstate.NewAutoRefresh(s.state) 399 snapstate.MockLastRefreshSchedule(af, schedule) 400 snapstate.MockNextRefresh(af, holdTime.Add(-2*time.Minute)) 401 402 s.state.Unlock() 403 err := af.Ensure() 404 s.state.Lock() 405 c.Check(err, IsNil) 406 407 // refresh did not happen yet 408 c.Check(s.store.ops, HasLen, 0) 409 410 // hold was reset 411 tr = config.NewTransaction(s.state) 412 var t1 time.Time 413 err = tr.Get("core", "refresh.hold", &t1) 414 c.Assert(config.IsNoOption(err), Equals, true) 415 416 // check next refresh 417 nextRefresh1 := af.NextRefresh() 418 c.Check(nextRefresh1.Before(nextRefresh), Equals, false) 419 } 420 421 func (s *autoRefreshTestSuite) TestEffectiveRefreshHold(c *C) { 422 s.state.Lock() 423 defer s.state.Unlock() 424 425 // assume no seed-time 426 s.state.Set("seed-time", nil) 427 428 af := snapstate.NewAutoRefresh(s.state) 429 430 t0, err := af.EffectiveRefreshHold() 431 c.Assert(err, IsNil) 432 c.Check(t0.IsZero(), Equals, true) 433 434 holdTime := time.Now() 435 tr := config.NewTransaction(s.state) 436 tr.Set("core", "refresh.hold", holdTime) 437 tr.Commit() 438 439 seedTime := holdTime.Add(-70 * 24 * time.Hour) 440 s.state.Set("seed-time", seedTime) 441 442 t1, err := af.EffectiveRefreshHold() 443 c.Assert(err, IsNil) 444 c.Check(t1.Equal(seedTime.Add(60*24*time.Hour)), Equals, true) 445 446 lastRefresh := holdTime.Add(-65 * 24 * time.Hour) 447 s.state.Set("last-refresh", lastRefresh) 448 449 t1, err = af.EffectiveRefreshHold() 450 c.Assert(err, IsNil) 451 c.Check(t1.Equal(lastRefresh.Add(60*24*time.Hour)), Equals, true) 452 453 s.state.Set("last-refresh", holdTime.Add(-6*time.Hour)) 454 t1, err = af.EffectiveRefreshHold() 455 c.Assert(err, IsNil) 456 c.Check(t1.Equal(holdTime), Equals, true) 457 } 458 459 func (s *autoRefreshTestSuite) TestEnsureLastRefreshAnchor(c *C) { 460 s.state.Lock() 461 defer s.state.Unlock() 462 // set hold => no refreshes 463 t0 := time.Now() 464 holdTime := t0.Add(1 * time.Hour) 465 tr := config.NewTransaction(s.state) 466 tr.Set("core", "refresh.hold", holdTime) 467 tr.Commit() 468 469 // with seed-time 470 s.state.Set("seed-time", t0.Add(-1*time.Hour)) 471 472 af := snapstate.NewAutoRefresh(s.state) 473 s.state.Unlock() 474 err := af.Ensure() 475 s.state.Lock() 476 c.Check(err, IsNil) 477 // no refresh 478 c.Check(s.store.ops, HasLen, 0) 479 lastRefresh, err := af.LastRefresh() 480 c.Assert(err, IsNil) 481 c.Check(lastRefresh.IsZero(), Equals, true) 482 483 // no seed-time 484 s.state.Set("seed-time", nil) 485 486 // fallback to time of executable 487 st, err := os.Stat("/proc/self/exe") 488 c.Assert(err, IsNil) 489 exeTime := st.ModTime() 490 491 af = snapstate.NewAutoRefresh(s.state) 492 s.state.Unlock() 493 err = af.Ensure() 494 s.state.Lock() 495 c.Check(err, IsNil) 496 // no refresh 497 c.Check(s.store.ops, HasLen, 0) 498 lastRefresh, err = af.LastRefresh() 499 c.Assert(err, IsNil) 500 c.Check(lastRefresh.Equal(exeTime), Equals, true) 501 502 // clear 503 s.state.Set("last-refresh", nil) 504 // use core last refresh time 505 coreCurrent := filepath.Join(dirs.SnapMountDir, "core", "current") 506 err = os.MkdirAll(coreCurrent, 0755) 507 c.Assert(err, IsNil) 508 st, err = os.Stat(coreCurrent) 509 c.Assert(err, IsNil) 510 coreRefreshed := st.ModTime() 511 512 af = snapstate.NewAutoRefresh(s.state) 513 s.state.Unlock() 514 err = af.Ensure() 515 s.state.Lock() 516 c.Check(err, IsNil) 517 // no refresh 518 c.Check(s.store.ops, HasLen, 0) 519 lastRefresh, err = af.LastRefresh() 520 c.Assert(err, IsNil) 521 c.Check(lastRefresh.Equal(coreRefreshed), Equals, true) 522 } 523 524 func (s *autoRefreshTestSuite) TestAtSeedPolicy(c *C) { 525 r := release.MockOnClassic(false) 526 defer r() 527 528 s.state.Lock() 529 defer s.state.Unlock() 530 531 af := snapstate.NewAutoRefresh(s.state) 532 533 // on core, does nothing 534 err := af.AtSeed() 535 c.Assert(err, IsNil) 536 c.Check(af.NextRefresh().IsZero(), Equals, true) 537 tr := config.NewTransaction(s.state) 538 var t1 time.Time 539 err = tr.Get("core", "refresh.hold", &t1) 540 c.Check(config.IsNoOption(err), Equals, true) 541 542 release.MockOnClassic(true) 543 now := time.Now() 544 // on classic it sets a refresh hold of 2h 545 err = af.AtSeed() 546 c.Assert(err, IsNil) 547 c.Check(af.NextRefresh().IsZero(), Equals, false) 548 tr = config.NewTransaction(s.state) 549 err = tr.Get("core", "refresh.hold", &t1) 550 c.Check(err, IsNil) 551 c.Check(t1.Before(now.Add(2*time.Hour)), Equals, false) 552 c.Check(t1.After(now.Add(2*time.Hour+5*time.Minute)), Equals, false) 553 554 // nop 555 err = af.AtSeed() 556 c.Assert(err, IsNil) 557 var t2 time.Time 558 tr = config.NewTransaction(s.state) 559 err = tr.Get("core", "refresh.hold", &t2) 560 c.Check(err, IsNil) 561 c.Check(t1.Equal(t2), Equals, true) 562 } 563 564 func (s *autoRefreshTestSuite) TestCanRefreshOnMetered(c *C) { 565 s.state.Lock() 566 defer s.state.Unlock() 567 568 can, err := snapstate.CanRefreshOnMeteredConnection(s.state) 569 c.Assert(can, Equals, true) 570 c.Assert(err, Equals, nil) 571 572 // enable holding refreshes when on metered connection 573 tr := config.NewTransaction(s.state) 574 err = tr.Set("core", "refresh.metered", "hold") 575 c.Assert(err, IsNil) 576 tr.Commit() 577 578 can, err = snapstate.CanRefreshOnMeteredConnection(s.state) 579 c.Assert(can, Equals, false) 580 c.Assert(err, Equals, nil) 581 582 // explicitly disable holding refreshes when on metered connection 583 tr = config.NewTransaction(s.state) 584 err = tr.Set("core", "refresh.metered", "") 585 c.Assert(err, IsNil) 586 tr.Commit() 587 588 can, err = snapstate.CanRefreshOnMeteredConnection(s.state) 589 c.Assert(can, Equals, true) 590 c.Assert(err, Equals, nil) 591 } 592 593 func (s *autoRefreshTestSuite) TestRefreshOnMeteredConnIsMetered(c *C) { 594 // pretend we're on metered connection 595 revert := snapstate.MockIsOnMeteredConnection(func() (bool, error) { 596 return true, nil 597 }) 598 defer revert() 599 600 s.state.Lock() 601 defer s.state.Unlock() 602 603 tr := config.NewTransaction(s.state) 604 tr.Set("core", "refresh.metered", "hold") 605 tr.Commit() 606 607 af := snapstate.NewAutoRefresh(s.state) 608 609 s.state.Set("last-refresh", time.Now().Add(-5*24*time.Hour)) 610 s.state.Unlock() 611 err := af.Ensure() 612 s.state.Lock() 613 c.Check(err, IsNil) 614 // no refresh 615 c.Check(s.store.ops, HasLen, 0) 616 617 c.Check(af.NextRefresh(), DeepEquals, time.Time{}) 618 619 // last refresh over 60 days ago, new one is launched regardless of 620 // connection being metered 621 s.state.Set("last-refresh", time.Now().Add(-61*24*time.Hour)) 622 s.state.Unlock() 623 err = af.Ensure() 624 s.state.Lock() 625 c.Check(err, IsNil) 626 c.Check(s.store.ops, DeepEquals, []string{"list-refresh"}) 627 } 628 629 func (s *autoRefreshTestSuite) TestRefreshOnMeteredConnNotMetered(c *C) { 630 // pretend we're on non-metered connection 631 revert := snapstate.MockIsOnMeteredConnection(func() (bool, error) { 632 return false, nil 633 }) 634 defer revert() 635 636 s.state.Lock() 637 defer s.state.Unlock() 638 639 tr := config.NewTransaction(s.state) 640 tr.Set("core", "refresh.metered", "hold") 641 tr.Commit() 642 643 af := snapstate.NewAutoRefresh(s.state) 644 645 s.state.Set("last-refresh", time.Now().Add(-5*24*time.Hour)) 646 s.state.Unlock() 647 err := af.Ensure() 648 s.state.Lock() 649 c.Check(err, IsNil) 650 c.Check(s.store.ops, DeepEquals, []string{"list-refresh"}) 651 }