github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/cmd/snap-repair/runner_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2017-2020 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 main_test 21 22 import ( 23 "bytes" 24 "encoding/json" 25 "fmt" 26 "io" 27 "io/ioutil" 28 "net/http" 29 "net/http/httptest" 30 "net/url" 31 "os" 32 "path/filepath" 33 "strconv" 34 "strings" 35 "time" 36 37 . "gopkg.in/check.v1" 38 "gopkg.in/retry.v1" 39 40 "github.com/snapcore/snapd/arch" 41 "github.com/snapcore/snapd/asserts" 42 "github.com/snapcore/snapd/asserts/assertstest" 43 "github.com/snapcore/snapd/asserts/sysdb" 44 "github.com/snapcore/snapd/boot" 45 repair "github.com/snapcore/snapd/cmd/snap-repair" 46 "github.com/snapcore/snapd/dirs" 47 "github.com/snapcore/snapd/logger" 48 "github.com/snapcore/snapd/osutil" 49 "github.com/snapcore/snapd/snapdenv" 50 "github.com/snapcore/snapd/testutil" 51 ) 52 53 type baseRunnerSuite struct { 54 testutil.BaseTest 55 56 tmpdir string 57 58 seedTime time.Time 59 t0 time.Time 60 61 storeSigning *assertstest.StoreStack 62 63 brandSigning *assertstest.SigningDB 64 brandAcct *asserts.Account 65 brandAcctKey *asserts.AccountKey 66 67 modelAs *asserts.Model 68 69 seedAssertsDir string 70 71 repairRootAcctKey *asserts.AccountKey 72 repairsAcctKey *asserts.AccountKey 73 74 repairsSigning *assertstest.SigningDB 75 76 restoreLogger func() 77 } 78 79 func makeReadOnly(c *C, dir string) (restore func()) { 80 // skip tests that need this because uid==0 does not honor 81 // write permissions in directories (yay, unix) 82 if os.Getuid() == 0 { 83 // FIXME: we could use osutil.Chattr() here 84 c.Skip("too lazy to make path readonly as root") 85 } 86 err := os.Chmod(dir, 0555) 87 c.Assert(err, IsNil) 88 return func() { 89 err := os.Chmod(dir, 0755) 90 c.Assert(err, IsNil) 91 } 92 } 93 94 func (s *baseRunnerSuite) SetUpSuite(c *C) { 95 s.storeSigning = assertstest.NewStoreStack("canonical", nil) 96 97 brandPrivKey, _ := assertstest.GenerateKey(752) 98 99 s.brandAcct = assertstest.NewAccount(s.storeSigning, "my-brand", map[string]interface{}{ 100 "account-id": "my-brand", 101 }, "") 102 s.brandAcctKey = assertstest.NewAccountKey(s.storeSigning, s.brandAcct, nil, brandPrivKey.PublicKey(), "") 103 s.brandSigning = assertstest.NewSigningDB("my-brand", brandPrivKey) 104 105 modelAs, err := s.brandSigning.Sign(asserts.ModelType, map[string]interface{}{ 106 "series": "16", 107 "brand-id": "my-brand", 108 "model": "my-model-2", 109 "architecture": "armhf", 110 "gadget": "gadget", 111 "kernel": "kernel", 112 "timestamp": time.Now().UTC().Format(time.RFC3339), 113 }, nil, "") 114 c.Assert(err, IsNil) 115 s.modelAs = modelAs.(*asserts.Model) 116 117 repairRootKey, _ := assertstest.GenerateKey(1024) 118 119 s.repairRootAcctKey = assertstest.NewAccountKey(s.storeSigning.RootSigning, s.storeSigning.TrustedAccount, nil, repairRootKey.PublicKey(), "") 120 121 repairsKey, _ := assertstest.GenerateKey(752) 122 123 repairRootSigning := assertstest.NewSigningDB("canonical", repairRootKey) 124 125 s.repairsAcctKey = assertstest.NewAccountKey(repairRootSigning, s.storeSigning.TrustedAccount, nil, repairsKey.PublicKey(), "") 126 127 s.repairsSigning = assertstest.NewSigningDB("canonical", repairsKey) 128 } 129 130 func (s *baseRunnerSuite) SetUpTest(c *C) { 131 s.BaseTest.SetUpTest(c) 132 133 _, restoreLogger := logger.MockLogger() 134 s.AddCleanup(restoreLogger) 135 136 s.tmpdir = c.MkDir() 137 dirs.SetRootDir(s.tmpdir) 138 s.AddCleanup(func() { dirs.SetRootDir("/") }) 139 } 140 141 func (s *baseRunnerSuite) signSeqRepairs(c *C, repairs []string) []string { 142 var seq []string 143 for _, rpr := range repairs { 144 decoded, err := asserts.Decode([]byte(rpr)) 145 c.Assert(err, IsNil) 146 signed, err := s.repairsSigning.Sign(asserts.RepairType, decoded.Headers(), decoded.Body(), "") 147 c.Assert(err, IsNil) 148 buf := &bytes.Buffer{} 149 enc := asserts.NewEncoder(buf) 150 enc.Encode(signed) 151 enc.Encode(s.repairsAcctKey) 152 seq = append(seq, buf.String()) 153 } 154 return seq 155 } 156 157 func checkStateJSON(c *C, file string, exp map[string]interface{}) { 158 stateFile := map[string]interface{}{} 159 b, err := ioutil.ReadFile(file) 160 c.Assert(err, IsNil) 161 err = json.Unmarshal(b, &stateFile) 162 c.Assert(err, IsNil) 163 c.Check(stateFile, DeepEquals, exp) 164 } 165 166 func (s *baseRunnerSuite) freshState(c *C) { 167 // assume base: core18 168 s.freshStateWithBaseAndMode(c, "core18", "") 169 } 170 171 func (s *baseRunnerSuite) freshStateWithBaseAndMode(c *C, base, mode string) { 172 err := os.MkdirAll(dirs.SnapRepairDir, 0775) 173 c.Assert(err, IsNil) 174 stateJSON := map[string]interface{}{ 175 "device": map[string]string{ 176 "brand": "my-brand", 177 "model": "my-model", 178 "base": base, 179 "mode": mode, 180 }, 181 "time-lower-bound": "2017-08-11T15:49:49Z", 182 } 183 b, err := json.Marshal(stateJSON) 184 c.Assert(err, IsNil) 185 186 err = ioutil.WriteFile(dirs.SnapRepairStateFile, b, 0600) 187 c.Assert(err, IsNil) 188 } 189 190 type runnerSuite struct { 191 baseRunnerSuite 192 193 restore func() 194 } 195 196 func (s *runnerSuite) SetUpSuite(c *C) { 197 s.baseRunnerSuite.SetUpSuite(c) 198 s.restore = snapdenv.SetUserAgentFromVersion("1", nil, "snap-repair") 199 } 200 201 func (s *runnerSuite) TearDownSuite(c *C) { 202 s.restore() 203 } 204 205 var _ = Suite(&runnerSuite{}) 206 207 var ( 208 testKey = `type: account-key 209 authority-id: canonical 210 account-id: canonical 211 name: repair 212 public-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 213 since: 2015-11-16T15:04:00Z 214 body-length: 149 215 sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij 216 217 AcZrBFaFwYABAvCX5A8dTcdLdhdiuy2YRHO5CAfM5InQefkKOhNMUq2yfi3Sk6trUHxskhZkPnm4 218 NKx2yRr332q7AJXQHLX+DrZ29ycyoQ2NQGO3eAfQ0hjAAQFYBF8SSh5SutPu5XCVABEBAAE= 219 220 AXNpZw== 221 ` 222 223 testRepair = `type: repair 224 authority-id: canonical 225 brand-id: canonical 226 repair-id: 2 227 summary: repair two 228 architectures: 229 - amd64 230 - arm64 231 series: 232 - 16 233 models: 234 - xyz/frobinator 235 timestamp: 2017-03-30T12:22:16Z 236 body-length: 7 237 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 238 239 script 240 241 242 AXNpZw== 243 ` 244 testHeadersResp = `{"headers": 245 {"architectures":["amd64","arm64"],"authority-id":"canonical","body-length":"7","brand-id":"canonical","models":["xyz/frobinator"],"repair-id":"2","series":["16"],"sign-key-sha3-384":"KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj","timestamp":"2017-03-30T12:22:16Z","type":"repair"}}` 246 ) 247 248 func mustParseURL(s string) *url.URL { 249 u, err := url.Parse(s) 250 if err != nil { 251 panic(err) 252 } 253 return u 254 } 255 256 func (s *runnerSuite) mockBrokenTimeNowSetToEpoch(c *C, runner *repair.Runner) (restore func()) { 257 epoch := time.Unix(0, 0) 258 r := repair.MockTimeNow(func() time.Time { 259 return epoch 260 }) 261 c.Check(runner.TLSTime().Equal(epoch), Equals, true) 262 return r 263 } 264 265 func (s *runnerSuite) checkBrokenTimeNowMitigated(c *C, runner *repair.Runner) { 266 c.Check(runner.TLSTime().Before(s.t0), Equals, false) 267 } 268 269 func (s *runnerSuite) TestFetchJustRepair(c *C) { 270 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 271 ua := r.Header.Get("User-Agent") 272 c.Check(strings.Contains(ua, "snap-repair"), Equals, true) 273 c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") 274 c.Check(r.URL.Path, Equals, "/repairs/canonical/2") 275 io.WriteString(w, testRepair) 276 })) 277 278 c.Assert(mockServer, NotNil) 279 defer mockServer.Close() 280 281 runner := repair.NewRunner() 282 runner.BaseURL = mustParseURL(mockServer.URL) 283 284 r := s.mockBrokenTimeNowSetToEpoch(c, runner) 285 defer r() 286 287 repair, aux, err := runner.Fetch("canonical", 2, -1) 288 c.Assert(err, IsNil) 289 c.Check(repair, NotNil) 290 c.Check(aux, HasLen, 0) 291 c.Check(repair.BrandID(), Equals, "canonical") 292 c.Check(repair.RepairID(), Equals, 2) 293 c.Check(repair.Body(), DeepEquals, []byte("script\n")) 294 295 s.checkBrokenTimeNowMitigated(c, runner) 296 } 297 298 func (s *runnerSuite) TestFetchScriptTooBig(c *C) { 299 restore := repair.MockMaxRepairScriptSize(4) 300 defer restore() 301 302 n := 0 303 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 304 n++ 305 c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") 306 c.Check(r.URL.Path, Equals, "/repairs/canonical/2") 307 io.WriteString(w, testRepair) 308 })) 309 310 c.Assert(mockServer, NotNil) 311 defer mockServer.Close() 312 313 runner := repair.NewRunner() 314 runner.BaseURL = mustParseURL(mockServer.URL) 315 316 _, _, err := runner.Fetch("canonical", 2, -1) 317 c.Assert(err, ErrorMatches, `assertion body length 7 exceeds maximum body size 4 for "repair".*`) 318 c.Assert(n, Equals, 1) 319 } 320 321 var ( 322 testRetryStrategy = retry.LimitCount(5, retry.LimitTime(1*time.Second, 323 retry.Exponential{ 324 Initial: 1 * time.Millisecond, 325 Factor: 1, 326 }, 327 )) 328 ) 329 330 func (s *runnerSuite) TestFetch500(c *C) { 331 restore := repair.MockFetchRetryStrategy(testRetryStrategy) 332 defer restore() 333 334 n := 0 335 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 336 n++ 337 w.WriteHeader(500) 338 })) 339 340 c.Assert(mockServer, NotNil) 341 defer mockServer.Close() 342 343 runner := repair.NewRunner() 344 runner.BaseURL = mustParseURL(mockServer.URL) 345 346 _, _, err := runner.Fetch("canonical", 2, -1) 347 c.Assert(err, ErrorMatches, "cannot fetch repair, unexpected status 500") 348 c.Assert(n, Equals, 5) 349 } 350 351 func (s *runnerSuite) TestFetchEmpty(c *C) { 352 restore := repair.MockFetchRetryStrategy(testRetryStrategy) 353 defer restore() 354 355 n := 0 356 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 357 n++ 358 w.WriteHeader(200) 359 })) 360 361 c.Assert(mockServer, NotNil) 362 defer mockServer.Close() 363 364 runner := repair.NewRunner() 365 runner.BaseURL = mustParseURL(mockServer.URL) 366 367 _, _, err := runner.Fetch("canonical", 2, -1) 368 c.Assert(err, Equals, io.ErrUnexpectedEOF) 369 c.Assert(n, Equals, 5) 370 } 371 372 func (s *runnerSuite) TestFetchBroken(c *C) { 373 restore := repair.MockFetchRetryStrategy(testRetryStrategy) 374 defer restore() 375 376 n := 0 377 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 378 n++ 379 w.WriteHeader(200) 380 io.WriteString(w, "xyz:") 381 })) 382 383 c.Assert(mockServer, NotNil) 384 defer mockServer.Close() 385 386 runner := repair.NewRunner() 387 runner.BaseURL = mustParseURL(mockServer.URL) 388 389 _, _, err := runner.Fetch("canonical", 2, -1) 390 c.Assert(err, Equals, io.ErrUnexpectedEOF) 391 c.Assert(n, Equals, 5) 392 } 393 394 func (s *runnerSuite) TestFetchNotFound(c *C) { 395 restore := repair.MockFetchRetryStrategy(testRetryStrategy) 396 defer restore() 397 398 n := 0 399 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 400 n++ 401 w.WriteHeader(404) 402 })) 403 404 c.Assert(mockServer, NotNil) 405 defer mockServer.Close() 406 407 runner := repair.NewRunner() 408 runner.BaseURL = mustParseURL(mockServer.URL) 409 410 r := s.mockBrokenTimeNowSetToEpoch(c, runner) 411 defer r() 412 413 _, _, err := runner.Fetch("canonical", 2, -1) 414 c.Assert(err, Equals, repair.ErrRepairNotFound) 415 c.Assert(n, Equals, 1) 416 417 s.checkBrokenTimeNowMitigated(c, runner) 418 } 419 420 func (s *runnerSuite) TestFetchIfNoneMatchNotModified(c *C) { 421 restore := repair.MockFetchRetryStrategy(testRetryStrategy) 422 defer restore() 423 424 n := 0 425 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 426 n++ 427 c.Check(r.Header.Get("If-None-Match"), Equals, `"0"`) 428 w.WriteHeader(304) 429 })) 430 431 c.Assert(mockServer, NotNil) 432 defer mockServer.Close() 433 434 runner := repair.NewRunner() 435 runner.BaseURL = mustParseURL(mockServer.URL) 436 437 r := s.mockBrokenTimeNowSetToEpoch(c, runner) 438 defer r() 439 440 _, _, err := runner.Fetch("canonical", 2, 0) 441 c.Assert(err, Equals, repair.ErrRepairNotModified) 442 c.Assert(n, Equals, 1) 443 444 s.checkBrokenTimeNowMitigated(c, runner) 445 } 446 447 func (s *runnerSuite) TestFetchIgnoreSupersededRevision(c *C) { 448 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 449 io.WriteString(w, testRepair) 450 })) 451 452 c.Assert(mockServer, NotNil) 453 defer mockServer.Close() 454 455 runner := repair.NewRunner() 456 runner.BaseURL = mustParseURL(mockServer.URL) 457 458 _, _, err := runner.Fetch("canonical", 2, 2) 459 c.Assert(err, Equals, repair.ErrRepairNotModified) 460 } 461 462 func (s *runnerSuite) TestFetchIdMismatch(c *C) { 463 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 464 c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") 465 io.WriteString(w, testRepair) 466 })) 467 468 c.Assert(mockServer, NotNil) 469 defer mockServer.Close() 470 471 runner := repair.NewRunner() 472 runner.BaseURL = mustParseURL(mockServer.URL) 473 474 _, _, err := runner.Fetch("canonical", 4, -1) 475 c.Assert(err, ErrorMatches, `cannot fetch repair, repair id mismatch canonical/2 != canonical/4`) 476 } 477 478 func (s *runnerSuite) TestFetchWrongFirstType(c *C) { 479 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 480 c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") 481 c.Check(r.URL.Path, Equals, "/repairs/canonical/2") 482 io.WriteString(w, testKey) 483 })) 484 485 c.Assert(mockServer, NotNil) 486 defer mockServer.Close() 487 488 runner := repair.NewRunner() 489 runner.BaseURL = mustParseURL(mockServer.URL) 490 491 _, _, err := runner.Fetch("canonical", 2, -1) 492 c.Assert(err, ErrorMatches, `cannot fetch repair, unexpected first assertion "account-key"`) 493 } 494 495 func (s *runnerSuite) TestFetchRepairPlusKey(c *C) { 496 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 497 c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") 498 c.Check(r.URL.Path, Equals, "/repairs/canonical/2") 499 io.WriteString(w, testRepair) 500 io.WriteString(w, "\n") 501 io.WriteString(w, testKey) 502 })) 503 504 c.Assert(mockServer, NotNil) 505 defer mockServer.Close() 506 507 runner := repair.NewRunner() 508 runner.BaseURL = mustParseURL(mockServer.URL) 509 510 repair, aux, err := runner.Fetch("canonical", 2, -1) 511 c.Assert(err, IsNil) 512 c.Check(repair, NotNil) 513 c.Check(aux, HasLen, 1) 514 _, ok := aux[0].(*asserts.AccountKey) 515 c.Check(ok, Equals, true) 516 } 517 518 func (s *runnerSuite) TestPeek(c *C) { 519 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 520 ua := r.Header.Get("User-Agent") 521 c.Check(strings.Contains(ua, "snap-repair"), Equals, true) 522 c.Check(r.Header.Get("Accept"), Equals, "application/json") 523 c.Check(r.URL.Path, Equals, "/repairs/canonical/2") 524 io.WriteString(w, testHeadersResp) 525 })) 526 527 c.Assert(mockServer, NotNil) 528 defer mockServer.Close() 529 530 runner := repair.NewRunner() 531 runner.BaseURL = mustParseURL(mockServer.URL) 532 533 r := s.mockBrokenTimeNowSetToEpoch(c, runner) 534 defer r() 535 536 h, err := runner.Peek("canonical", 2) 537 c.Assert(err, IsNil) 538 c.Check(h["series"], DeepEquals, []interface{}{"16"}) 539 c.Check(h["architectures"], DeepEquals, []interface{}{"amd64", "arm64"}) 540 c.Check(h["models"], DeepEquals, []interface{}{"xyz/frobinator"}) 541 542 s.checkBrokenTimeNowMitigated(c, runner) 543 } 544 545 func (s *runnerSuite) TestPeek500(c *C) { 546 restore := repair.MockPeekRetryStrategy(testRetryStrategy) 547 defer restore() 548 549 n := 0 550 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 551 n++ 552 w.WriteHeader(500) 553 })) 554 555 c.Assert(mockServer, NotNil) 556 defer mockServer.Close() 557 558 runner := repair.NewRunner() 559 runner.BaseURL = mustParseURL(mockServer.URL) 560 561 _, err := runner.Peek("canonical", 2) 562 c.Assert(err, ErrorMatches, "cannot peek repair headers, unexpected status 500") 563 c.Assert(n, Equals, 5) 564 } 565 566 func (s *runnerSuite) TestPeekInvalid(c *C) { 567 restore := repair.MockPeekRetryStrategy(testRetryStrategy) 568 defer restore() 569 570 n := 0 571 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 572 n++ 573 w.WriteHeader(200) 574 io.WriteString(w, "{") 575 })) 576 577 c.Assert(mockServer, NotNil) 578 defer mockServer.Close() 579 580 runner := repair.NewRunner() 581 runner.BaseURL = mustParseURL(mockServer.URL) 582 583 _, err := runner.Peek("canonical", 2) 584 c.Assert(err, Equals, io.ErrUnexpectedEOF) 585 c.Assert(n, Equals, 5) 586 } 587 588 func (s *runnerSuite) TestPeekNotFound(c *C) { 589 n := 0 590 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 591 n++ 592 w.WriteHeader(404) 593 })) 594 595 c.Assert(mockServer, NotNil) 596 defer mockServer.Close() 597 598 runner := repair.NewRunner() 599 runner.BaseURL = mustParseURL(mockServer.URL) 600 601 r := s.mockBrokenTimeNowSetToEpoch(c, runner) 602 defer r() 603 604 _, err := runner.Peek("canonical", 2) 605 c.Assert(err, Equals, repair.ErrRepairNotFound) 606 c.Assert(n, Equals, 1) 607 608 s.checkBrokenTimeNowMitigated(c, runner) 609 } 610 611 func (s *runnerSuite) TestPeekIdMismatch(c *C) { 612 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 613 c.Check(r.Header.Get("Accept"), Equals, "application/json") 614 io.WriteString(w, testHeadersResp) 615 })) 616 617 c.Assert(mockServer, NotNil) 618 defer mockServer.Close() 619 620 runner := repair.NewRunner() 621 runner.BaseURL = mustParseURL(mockServer.URL) 622 623 _, err := runner.Peek("canonical", 4) 624 c.Assert(err, ErrorMatches, `cannot peek repair headers, repair id mismatch canonical/2 != canonical/4`) 625 } 626 627 func (s *runnerSuite) TestLoadState(c *C) { 628 s.freshState(c) 629 630 runner := repair.NewRunner() 631 err := runner.LoadState() 632 c.Assert(err, IsNil) 633 brand, model := runner.BrandModel() 634 c.Check(brand, Equals, "my-brand") 635 c.Check(model, Equals, "my-model") 636 } 637 638 func (s *runnerSuite) TestLoadStateInitStateFail(c *C) { 639 err := os.MkdirAll(dirs.SnapSeedDir, 0755) 640 c.Assert(err, IsNil) 641 642 restore := makeReadOnly(c, filepath.Dir(dirs.SnapSeedDir)) 643 defer restore() 644 645 runner := repair.NewRunner() 646 err = runner.LoadState() 647 c.Check(err, ErrorMatches, `cannot create repair state directory:.*`) 648 } 649 650 func (s *runnerSuite) TestSaveStateFail(c *C) { 651 s.freshState(c) 652 653 runner := repair.NewRunner() 654 err := runner.LoadState() 655 c.Assert(err, IsNil) 656 657 restore := makeReadOnly(c, dirs.SnapRepairDir) 658 defer restore() 659 660 // no error because this is a no-op 661 err = runner.SaveState() 662 c.Check(err, IsNil) 663 664 // mark as modified 665 runner.SetStateModified(true) 666 667 err = runner.SaveState() 668 c.Check(err, ErrorMatches, `cannot save repair state:.*`) 669 } 670 671 func (s *runnerSuite) TestSaveState(c *C) { 672 s.freshState(c) 673 674 runner := repair.NewRunner() 675 err := runner.LoadState() 676 c.Assert(err, IsNil) 677 678 runner.SetSequence("canonical", []*repair.RepairState{ 679 {Sequence: 1, Revision: 3}, 680 }) 681 // mark as modified 682 runner.SetStateModified(true) 683 684 err = runner.SaveState() 685 c.Assert(err, IsNil) 686 687 exp := map[string]interface{}{ 688 "device": map[string]interface{}{ 689 "brand": "my-brand", 690 "model": "my-model", 691 "base": "core18", 692 "mode": "", 693 }, 694 "sequences": map[string]interface{}{ 695 "canonical": []interface{}{ 696 map[string]interface{}{ 697 // all json numbers are floats 698 "sequence": 1.0, 699 "revision": 3.0, 700 "status": 0.0, 701 }, 702 }, 703 }, 704 "time-lower-bound": "2017-08-11T15:49:49Z", 705 } 706 707 checkStateJSON(c, dirs.SnapRepairStateFile, exp) 708 } 709 710 type dev struct { 711 base string 712 mode string 713 } 714 715 func (s *runnerSuite) TestApplicable(c *C) { 716 717 scenarios := []struct { 718 device *dev 719 headers map[string]interface{} 720 applicable bool 721 }{ 722 {nil, nil, true}, 723 {nil, map[string]interface{}{"series": []interface{}{"18"}}, false}, 724 {nil, map[string]interface{}{"series": []interface{}{"18", "16"}}, true}, 725 {nil, map[string]interface{}{"series": "18"}, false}, 726 {nil, map[string]interface{}{"series": []interface{}{18}}, false}, 727 {nil, map[string]interface{}{"architectures": []interface{}{arch.DpkgArchitecture()}}, true}, 728 {nil, map[string]interface{}{"architectures": []interface{}{"other-arch"}}, false}, 729 {nil, map[string]interface{}{"architectures": []interface{}{"other-arch", arch.DpkgArchitecture()}}, true}, 730 {nil, map[string]interface{}{"architectures": arch.DpkgArchitecture()}, false}, 731 {nil, map[string]interface{}{"models": []interface{}{"my-brand/my-model"}}, true}, 732 {nil, map[string]interface{}{"models": []interface{}{"other-brand/other-model"}}, false}, 733 {nil, map[string]interface{}{"models": []interface{}{"other-brand/other-model", "my-brand/my-model"}}, true}, 734 {nil, map[string]interface{}{"models": "my-brand/my-model"}, false}, 735 // modes for uc16 / uc18 devices 736 {nil, map[string]interface{}{"modes": []interface{}{}}, true}, 737 {nil, map[string]interface{}{"modes": []interface{}{"run"}}, false}, 738 {nil, map[string]interface{}{"modes": []interface{}{"recover"}}, false}, 739 {nil, map[string]interface{}{"modes": []interface{}{"run", "recover"}}, false}, 740 // run mode for uc20 devices 741 {&dev{mode: "run"}, map[string]interface{}{"modes": []interface{}{}}, true}, 742 {&dev{mode: "run"}, map[string]interface{}{"modes": []interface{}{"run"}}, true}, 743 {&dev{mode: "run"}, map[string]interface{}{"modes": []interface{}{"recover"}}, false}, 744 {&dev{mode: "run"}, map[string]interface{}{"modes": []interface{}{"run", "recover"}}, true}, 745 // recover mode for uc20 devices 746 {&dev{mode: "recover"}, map[string]interface{}{"modes": []interface{}{}}, false}, 747 {&dev{mode: "recover"}, map[string]interface{}{"modes": []interface{}{"run"}}, false}, 748 {&dev{mode: "recover"}, map[string]interface{}{"modes": []interface{}{"recover"}}, true}, 749 {&dev{mode: "recover"}, map[string]interface{}{"modes": []interface{}{"run", "recover"}}, true}, 750 // bases for uc16 devices 751 {&dev{base: "core"}, map[string]interface{}{"bases": []interface{}{"core"}}, true}, 752 {&dev{base: "core"}, map[string]interface{}{"bases": []interface{}{"core18"}}, false}, 753 {&dev{base: "core"}, map[string]interface{}{"bases": []interface{}{"core", "core18"}}, true}, 754 // bases for uc18 devices 755 {&dev{base: "core18"}, map[string]interface{}{"bases": []interface{}{"core18"}}, true}, 756 {&dev{base: "core18"}, map[string]interface{}{"bases": []interface{}{"core"}}, false}, 757 {&dev{base: "core18"}, map[string]interface{}{"bases": []interface{}{"core", "core18"}}, true}, 758 // bases for uc20 devices 759 {&dev{base: "core20"}, map[string]interface{}{"bases": []interface{}{"core20"}}, true}, 760 {&dev{base: "core20"}, map[string]interface{}{"bases": []interface{}{"core"}}, false}, 761 {&dev{base: "core20"}, map[string]interface{}{"bases": []interface{}{"core", "core20"}}, true}, 762 // model prefix matches 763 {nil, map[string]interface{}{"models": []interface{}{"my-brand/*"}}, true}, 764 {nil, map[string]interface{}{"models": []interface{}{"my-brand/my-mod*"}}, true}, 765 {nil, map[string]interface{}{"models": []interface{}{"my-brand/xxx*"}}, false}, 766 {nil, map[string]interface{}{"models": []interface{}{"my-brand/my-mod*", "my-brand/xxx*"}}, true}, 767 {nil, map[string]interface{}{"models": []interface{}{"my*"}}, false}, 768 {nil, map[string]interface{}{"disabled": "true"}, false}, 769 {nil, map[string]interface{}{"disabled": "false"}, true}, 770 } 771 772 for _, scen := range scenarios { 773 if scen.device == nil { 774 s.freshState(c) 775 } else { 776 s.freshStateWithBaseAndMode(c, scen.device.base, scen.device.mode) 777 } 778 779 runner := repair.NewRunner() 780 err := runner.LoadState() 781 c.Assert(err, IsNil) 782 783 ok := runner.Applicable(scen.headers) 784 c.Check(ok, Equals, scen.applicable, Commentf("%v", scen)) 785 } 786 } 787 788 var ( 789 nextRepairs = []string{`type: repair 790 authority-id: canonical 791 brand-id: canonical 792 repair-id: 1 793 summary: repair one 794 timestamp: 2017-07-01T12:00:00Z 795 body-length: 8 796 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 797 798 scriptA 799 800 801 AXNpZw==`, 802 `type: repair 803 authority-id: canonical 804 brand-id: canonical 805 repair-id: 2 806 summary: repair two 807 series: 808 - 33 809 timestamp: 2017-07-02T12:00:00Z 810 body-length: 8 811 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 812 813 scriptB 814 815 816 AXNpZw==`, 817 `type: repair 818 revision: 2 819 authority-id: canonical 820 brand-id: canonical 821 repair-id: 3 822 summary: repair three rev2 823 series: 824 - 16 825 timestamp: 2017-07-03T12:00:00Z 826 body-length: 8 827 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 828 829 scriptC 830 831 832 AXNpZw== 833 `} 834 835 repair3Rev4 = `type: repair 836 revision: 4 837 authority-id: canonical 838 brand-id: canonical 839 repair-id: 3 840 summary: repair three rev4 841 series: 842 - 16 843 timestamp: 2017-07-03T12:00:00Z 844 body-length: 9 845 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 846 847 scriptC2 848 849 850 AXNpZw== 851 ` 852 853 repair4 = `type: repair 854 authority-id: canonical 855 brand-id: canonical 856 repair-id: 4 857 summary: repair four 858 timestamp: 2017-07-03T12:00:00Z 859 body-length: 8 860 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 861 862 scriptD 863 864 865 AXNpZw== 866 ` 867 ) 868 869 func makeMockServer(c *C, seqRepairs *[]string, redirectFirst bool) *httptest.Server { 870 var mockServer *httptest.Server 871 mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 872 ua := r.Header.Get("User-Agent") 873 c.Check(strings.Contains(ua, "snap-repair"), Equals, true) 874 875 urlPath := r.URL.Path 876 if redirectFirst && r.Header.Get("Accept") == asserts.MediaType { 877 if !strings.HasPrefix(urlPath, "/final/") { 878 // redirect 879 finalURL := mockServer.URL + "/final" + r.URL.Path 880 w.Header().Set("Location", finalURL) 881 w.WriteHeader(302) 882 return 883 } 884 urlPath = strings.TrimPrefix(urlPath, "/final") 885 } 886 887 c.Check(strings.HasPrefix(urlPath, "/repairs/canonical/"), Equals, true) 888 889 seq, err := strconv.Atoi(strings.TrimPrefix(urlPath, "/repairs/canonical/")) 890 c.Assert(err, IsNil) 891 892 if seq > len(*seqRepairs) { 893 w.WriteHeader(404) 894 return 895 } 896 897 rpr := []byte((*seqRepairs)[seq-1]) 898 dec := asserts.NewDecoder(bytes.NewBuffer(rpr)) 899 repair, err := dec.Decode() 900 c.Assert(err, IsNil) 901 902 switch r.Header.Get("Accept") { 903 case "application/json": 904 b, err := json.Marshal(map[string]interface{}{ 905 "headers": repair.Headers(), 906 }) 907 c.Assert(err, IsNil) 908 w.Write(b) 909 case asserts.MediaType: 910 etag := fmt.Sprintf(`"%d"`, repair.Revision()) 911 if strings.Contains(r.Header.Get("If-None-Match"), etag) { 912 w.WriteHeader(304) 913 return 914 } 915 w.Write(rpr) 916 } 917 })) 918 919 c.Assert(mockServer, NotNil) 920 921 return mockServer 922 } 923 924 func (s *runnerSuite) TestTrustedRepairRootKeys(c *C) { 925 acctKeys := repair.TrustedRepairRootKeys() 926 c.Check(acctKeys, HasLen, 1) 927 c.Check(acctKeys[0].AccountID(), Equals, "canonical") 928 c.Check(acctKeys[0].PublicKeyID(), Equals, "nttW6NfBXI_E-00u38W-KH6eiksfQNXuI7IiumoV49_zkbhM0sYTzSnFlwZC-W4t") 929 } 930 931 func (s *runnerSuite) TestVerify(c *C) { 932 r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) 933 defer r1() 934 r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) 935 defer r2() 936 937 runner := repair.NewRunner() 938 939 a, err := s.repairsSigning.Sign(asserts.RepairType, map[string]interface{}{ 940 "brand-id": "canonical", 941 "repair-id": "2", 942 "summary": "repair two", 943 "timestamp": time.Now().UTC().Format(time.RFC3339), 944 }, []byte("#script"), "") 945 c.Assert(err, IsNil) 946 rpr := a.(*asserts.Repair) 947 948 err = runner.Verify(rpr, []asserts.Assertion{s.repairsAcctKey}) 949 c.Check(err, IsNil) 950 } 951 952 func (s *runnerSuite) loadSequences(c *C) map[string][]*repair.RepairState { 953 data, err := ioutil.ReadFile(dirs.SnapRepairStateFile) 954 c.Assert(err, IsNil) 955 var x struct { 956 Sequences map[string][]*repair.RepairState `json:"sequences"` 957 } 958 err = json.Unmarshal(data, &x) 959 c.Assert(err, IsNil) 960 return x.Sequences 961 } 962 963 func (s *runnerSuite) testNext(c *C, redirectFirst bool) { 964 r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) 965 defer r1() 966 r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) 967 defer r2() 968 969 seqRepairs := s.signSeqRepairs(c, nextRepairs) 970 971 mockServer := makeMockServer(c, &seqRepairs, redirectFirst) 972 defer mockServer.Close() 973 974 runner := repair.NewRunner() 975 runner.BaseURL = mustParseURL(mockServer.URL) 976 runner.LoadState() 977 978 rpr, err := runner.Next("canonical") 979 c.Assert(err, IsNil) 980 c.Check(rpr.RepairID(), Equals, 1) 981 c.Check(osutil.FileExists(filepath.Join(dirs.SnapRepairAssertsDir, "canonical", "1", "r0.repair")), Equals, true) 982 983 rpr, err = runner.Next("canonical") 984 c.Assert(err, IsNil) 985 c.Check(rpr.RepairID(), Equals, 3) 986 c.Check(filepath.Join(dirs.SnapRepairAssertsDir, "canonical", "3", "r2.repair"), testutil.FileEquals, seqRepairs[2]) 987 988 // no more 989 rpr, err = runner.Next("canonical") 990 c.Check(err, Equals, repair.ErrRepairNotFound) 991 992 expectedSeq := []*repair.RepairState{ 993 {Sequence: 1}, 994 {Sequence: 2, Status: repair.SkipStatus}, 995 {Sequence: 3, Revision: 2}, 996 } 997 c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) 998 // on disk 999 seqs := s.loadSequences(c) 1000 c.Check(seqs["canonical"], DeepEquals, expectedSeq) 1001 1002 // start fresh run with new runner 1003 // will refetch repair 3 1004 signed := s.signSeqRepairs(c, []string{repair3Rev4, repair4}) 1005 seqRepairs[2] = signed[0] 1006 seqRepairs = append(seqRepairs, signed[1]) 1007 1008 runner = repair.NewRunner() 1009 runner.BaseURL = mustParseURL(mockServer.URL) 1010 runner.LoadState() 1011 1012 rpr, err = runner.Next("canonical") 1013 c.Assert(err, IsNil) 1014 c.Check(rpr.RepairID(), Equals, 1) 1015 1016 rpr, err = runner.Next("canonical") 1017 c.Assert(err, IsNil) 1018 c.Check(rpr.RepairID(), Equals, 3) 1019 // refetched new revision! 1020 c.Check(rpr.Revision(), Equals, 4) 1021 c.Check(rpr.Body(), DeepEquals, []byte("scriptC2\n")) 1022 1023 // new repair 1024 rpr, err = runner.Next("canonical") 1025 c.Assert(err, IsNil) 1026 c.Check(rpr.RepairID(), Equals, 4) 1027 c.Check(rpr.Body(), DeepEquals, []byte("scriptD\n")) 1028 1029 // no more 1030 rpr, err = runner.Next("canonical") 1031 c.Check(err, Equals, repair.ErrRepairNotFound) 1032 1033 c.Check(runner.Sequence("canonical"), DeepEquals, []*repair.RepairState{ 1034 {Sequence: 1}, 1035 {Sequence: 2, Status: repair.SkipStatus}, 1036 {Sequence: 3, Revision: 4}, 1037 {Sequence: 4}, 1038 }) 1039 } 1040 1041 func (s *runnerSuite) TestNext(c *C) { 1042 redirectFirst := false 1043 s.testNext(c, redirectFirst) 1044 } 1045 1046 func (s *runnerSuite) TestNextRedirect(c *C) { 1047 redirectFirst := true 1048 s.testNext(c, redirectFirst) 1049 } 1050 1051 func (s *runnerSuite) TestNextImmediateSkip(c *C) { 1052 seqRepairs := []string{`type: repair 1053 authority-id: canonical 1054 brand-id: canonical 1055 repair-id: 1 1056 summary: repair one 1057 series: 1058 - 33 1059 timestamp: 2017-07-02T12:00:00Z 1060 body-length: 8 1061 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 1062 1063 scriptB 1064 1065 1066 AXNpZw==`} 1067 1068 mockServer := makeMockServer(c, &seqRepairs, false) 1069 defer mockServer.Close() 1070 1071 runner := repair.NewRunner() 1072 runner.BaseURL = mustParseURL(mockServer.URL) 1073 runner.LoadState() 1074 1075 // not applicable => not returned 1076 _, err := runner.Next("canonical") 1077 c.Check(err, Equals, repair.ErrRepairNotFound) 1078 1079 expectedSeq := []*repair.RepairState{ 1080 {Sequence: 1, Status: repair.SkipStatus}, 1081 } 1082 c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) 1083 // on disk 1084 seqs := s.loadSequences(c) 1085 c.Check(seqs["canonical"], DeepEquals, expectedSeq) 1086 } 1087 1088 func (s *runnerSuite) TestNextRefetchSkip(c *C) { 1089 seqRepairs := []string{`type: repair 1090 authority-id: canonical 1091 brand-id: canonical 1092 repair-id: 1 1093 summary: repair one 1094 series: 1095 - 16 1096 timestamp: 2017-07-02T12:00:00Z 1097 body-length: 8 1098 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 1099 1100 scriptB 1101 1102 1103 AXNpZw==`} 1104 1105 r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) 1106 defer r1() 1107 r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) 1108 defer r2() 1109 1110 seqRepairs = s.signSeqRepairs(c, seqRepairs) 1111 1112 mockServer := makeMockServer(c, &seqRepairs, false) 1113 defer mockServer.Close() 1114 1115 runner := repair.NewRunner() 1116 runner.BaseURL = mustParseURL(mockServer.URL) 1117 runner.LoadState() 1118 1119 _, err := runner.Next("canonical") 1120 c.Assert(err, IsNil) 1121 1122 expectedSeq := []*repair.RepairState{ 1123 {Sequence: 1}, 1124 } 1125 c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) 1126 // on disk 1127 seqs := s.loadSequences(c) 1128 c.Check(seqs["canonical"], DeepEquals, expectedSeq) 1129 1130 // new fresh run, repair becomes now unapplicable 1131 seqRepairs[0] = `type: repair 1132 authority-id: canonical 1133 revision: 1 1134 brand-id: canonical 1135 repair-id: 1 1136 summary: repair one rev1 1137 series: 1138 - 16 1139 disabled: true 1140 timestamp: 2017-07-02T12:00:00Z 1141 body-length: 7 1142 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 1143 1144 scriptX 1145 1146 AXNpZw==` 1147 1148 seqRepairs = s.signSeqRepairs(c, seqRepairs) 1149 1150 runner = repair.NewRunner() 1151 runner.BaseURL = mustParseURL(mockServer.URL) 1152 runner.LoadState() 1153 1154 _, err = runner.Next("canonical") 1155 c.Check(err, Equals, repair.ErrRepairNotFound) 1156 1157 expectedSeq = []*repair.RepairState{ 1158 {Sequence: 1, Revision: 1, Status: repair.SkipStatus}, 1159 } 1160 c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) 1161 // on disk 1162 seqs = s.loadSequences(c) 1163 c.Check(seqs["canonical"], DeepEquals, expectedSeq) 1164 } 1165 1166 func (s *runnerSuite) TestNext500(c *C) { 1167 restore := repair.MockPeekRetryStrategy(testRetryStrategy) 1168 defer restore() 1169 1170 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1171 w.WriteHeader(500) 1172 })) 1173 1174 c.Assert(mockServer, NotNil) 1175 defer mockServer.Close() 1176 1177 runner := repair.NewRunner() 1178 runner.BaseURL = mustParseURL(mockServer.URL) 1179 runner.LoadState() 1180 1181 _, err := runner.Next("canonical") 1182 c.Assert(err, ErrorMatches, "cannot peek repair headers, unexpected status 500") 1183 } 1184 1185 func (s *runnerSuite) TestNextNotFound(c *C) { 1186 s.freshState(c) 1187 1188 restore := repair.MockPeekRetryStrategy(testRetryStrategy) 1189 defer restore() 1190 1191 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1192 w.WriteHeader(404) 1193 })) 1194 1195 c.Assert(mockServer, NotNil) 1196 defer mockServer.Close() 1197 1198 runner := repair.NewRunner() 1199 runner.BaseURL = mustParseURL(mockServer.URL) 1200 runner.LoadState() 1201 1202 _, err := runner.Next("canonical") 1203 c.Assert(err, Equals, repair.ErrRepairNotFound) 1204 1205 // we saved new time lower bound 1206 stateFileExp := map[string]interface{}{ 1207 "device": map[string]interface{}{ 1208 "brand": "my-brand", 1209 "model": "my-model", 1210 "base": "core18", 1211 "mode": "", 1212 }, 1213 "time-lower-bound": runner.TimeLowerBound().Format(time.RFC3339), 1214 } 1215 1216 checkStateJSON(c, dirs.SnapRepairStateFile, stateFileExp) 1217 } 1218 1219 func (s *runnerSuite) TestNextSaveStateError(c *C) { 1220 seqRepairs := []string{`type: repair 1221 authority-id: canonical 1222 brand-id: canonical 1223 repair-id: 1 1224 summary: repair one 1225 series: 1226 - 33 1227 timestamp: 2017-07-02T12:00:00Z 1228 body-length: 8 1229 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 1230 1231 scriptB 1232 1233 1234 AXNpZw==`} 1235 1236 mockServer := makeMockServer(c, &seqRepairs, false) 1237 defer mockServer.Close() 1238 1239 runner := repair.NewRunner() 1240 runner.BaseURL = mustParseURL(mockServer.URL) 1241 runner.LoadState() 1242 1243 // break SaveState 1244 restore := makeReadOnly(c, dirs.SnapRepairDir) 1245 defer restore() 1246 1247 _, err := runner.Next("canonical") 1248 c.Check(err, ErrorMatches, `cannot save repair state:.*`) 1249 } 1250 1251 func (s *runnerSuite) TestNextVerifyNoKey(c *C) { 1252 seqRepairs := []string{`type: repair 1253 authority-id: canonical 1254 brand-id: canonical 1255 repair-id: 1 1256 summary: repair one 1257 timestamp: 2017-07-02T12:00:00Z 1258 body-length: 8 1259 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 1260 1261 scriptB 1262 1263 1264 AXNpZw==`} 1265 1266 mockServer := makeMockServer(c, &seqRepairs, false) 1267 defer mockServer.Close() 1268 1269 runner := repair.NewRunner() 1270 runner.BaseURL = mustParseURL(mockServer.URL) 1271 runner.LoadState() 1272 1273 _, err := runner.Next("canonical") 1274 c.Check(err, ErrorMatches, `cannot verify repair canonical-1: cannot find public key.*`) 1275 1276 c.Check(runner.Sequence("canonical"), HasLen, 0) 1277 } 1278 1279 func (s *runnerSuite) TestNextVerifySelfSigned(c *C) { 1280 randoKey, _ := assertstest.GenerateKey(752) 1281 1282 randomSigning := assertstest.NewSigningDB("canonical", randoKey) 1283 randoKeyEncoded, err := asserts.EncodePublicKey(randoKey.PublicKey()) 1284 c.Assert(err, IsNil) 1285 acctKey, err := randomSigning.Sign(asserts.AccountKeyType, map[string]interface{}{ 1286 "authority-id": "canonical", 1287 "account-id": "canonical", 1288 "public-key-sha3-384": randoKey.PublicKey().ID(), 1289 "name": "repairs", 1290 "since": time.Now().UTC().Format(time.RFC3339), 1291 }, randoKeyEncoded, "") 1292 c.Assert(err, IsNil) 1293 1294 rpr, err := randomSigning.Sign(asserts.RepairType, map[string]interface{}{ 1295 "brand-id": "canonical", 1296 "repair-id": "1", 1297 "summary": "repair one", 1298 "timestamp": time.Now().UTC().Format(time.RFC3339), 1299 }, []byte("scriptB\n"), "") 1300 c.Assert(err, IsNil) 1301 1302 buf := &bytes.Buffer{} 1303 enc := asserts.NewEncoder(buf) 1304 enc.Encode(rpr) 1305 enc.Encode(acctKey) 1306 seqRepairs := []string{buf.String()} 1307 1308 mockServer := makeMockServer(c, &seqRepairs, false) 1309 defer mockServer.Close() 1310 1311 runner := repair.NewRunner() 1312 runner.BaseURL = mustParseURL(mockServer.URL) 1313 runner.LoadState() 1314 1315 _, err = runner.Next("canonical") 1316 c.Check(err, ErrorMatches, `cannot verify repair canonical-1: circular assertions`) 1317 1318 c.Check(runner.Sequence("canonical"), HasLen, 0) 1319 } 1320 1321 func (s *runnerSuite) TestNextVerifyAllKeysOK(c *C) { 1322 r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) 1323 defer r1() 1324 r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) 1325 defer r2() 1326 1327 decoded, err := asserts.Decode([]byte(nextRepairs[0])) 1328 c.Assert(err, IsNil) 1329 signed, err := s.repairsSigning.Sign(asserts.RepairType, decoded.Headers(), decoded.Body(), "") 1330 c.Assert(err, IsNil) 1331 1332 // stream with all keys (any order) works as well 1333 buf := &bytes.Buffer{} 1334 enc := asserts.NewEncoder(buf) 1335 enc.Encode(signed) 1336 enc.Encode(s.storeSigning.TrustedKey) 1337 enc.Encode(s.repairRootAcctKey) 1338 enc.Encode(s.repairsAcctKey) 1339 seqRepairs := []string{buf.String()} 1340 1341 mockServer := makeMockServer(c, &seqRepairs, false) 1342 defer mockServer.Close() 1343 1344 runner := repair.NewRunner() 1345 runner.BaseURL = mustParseURL(mockServer.URL) 1346 runner.LoadState() 1347 1348 rpr, err := runner.Next("canonical") 1349 c.Assert(err, IsNil) 1350 c.Check(rpr.RepairID(), Equals, 1) 1351 } 1352 1353 func (s *runnerSuite) TestRepairSetStatus(c *C) { 1354 seqRepairs := []string{`type: repair 1355 authority-id: canonical 1356 brand-id: canonical 1357 repair-id: 1 1358 summary: repair one 1359 timestamp: 2017-07-02T12:00:00Z 1360 body-length: 8 1361 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 1362 1363 scriptB 1364 1365 1366 AXNpZw==`} 1367 1368 r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) 1369 defer r1() 1370 r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) 1371 defer r2() 1372 1373 seqRepairs = s.signSeqRepairs(c, seqRepairs) 1374 1375 mockServer := makeMockServer(c, &seqRepairs, false) 1376 defer mockServer.Close() 1377 1378 runner := repair.NewRunner() 1379 runner.BaseURL = mustParseURL(mockServer.URL) 1380 runner.LoadState() 1381 1382 rpr, err := runner.Next("canonical") 1383 c.Assert(err, IsNil) 1384 1385 rpr.SetStatus(repair.DoneStatus) 1386 1387 expectedSeq := []*repair.RepairState{ 1388 {Sequence: 1, Status: repair.DoneStatus}, 1389 } 1390 c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) 1391 // on disk 1392 seqs := s.loadSequences(c) 1393 c.Check(seqs["canonical"], DeepEquals, expectedSeq) 1394 } 1395 1396 func (s *runnerSuite) TestRepairBasicRun(c *C) { 1397 seqRepairs := []string{`type: repair 1398 authority-id: canonical 1399 brand-id: canonical 1400 repair-id: 1 1401 summary: repair one 1402 series: 1403 - 16 1404 timestamp: 2017-07-02T12:00:00Z 1405 body-length: 17 1406 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 1407 1408 #!/bin/sh 1409 exit 0 1410 1411 1412 AXNpZw==`} 1413 1414 r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) 1415 defer r1() 1416 r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) 1417 defer r2() 1418 1419 seqRepairs = s.signSeqRepairs(c, seqRepairs) 1420 1421 mockServer := makeMockServer(c, &seqRepairs, false) 1422 defer mockServer.Close() 1423 1424 runner := repair.NewRunner() 1425 runner.BaseURL = mustParseURL(mockServer.URL) 1426 runner.LoadState() 1427 1428 rpr, err := runner.Next("canonical") 1429 c.Assert(err, IsNil) 1430 1431 err = rpr.Run() 1432 c.Assert(err, IsNil) 1433 c.Check(filepath.Join(dirs.SnapRepairRunDir, "canonical", "1", "r0.script"), testutil.FileEquals, "#!/bin/sh\nexit 0\n") 1434 } 1435 1436 func (s *runnerSuite) TestRepairBasicRun20RecoverEnv(c *C) { 1437 seqRepairs := []string{`type: repair 1438 authority-id: canonical 1439 brand-id: canonical 1440 repair-id: 1 1441 summary: repair one 1442 series: 1443 - 16 1444 bases: 1445 - core20 1446 modes: 1447 - recover 1448 - run 1449 timestamp: 2017-07-02T12:00:00Z 1450 body-length: 81 1451 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 1452 1453 #!/bin/sh 1454 env | grep SNAP_SYSTEM_MODE 1455 echo "done" >&$SNAP_REPAIR_STATUS_FD 1456 exit 0 1457 1458 AXNpZw==`} 1459 1460 seqRepairs = s.signSeqRepairs(c, seqRepairs) 1461 1462 r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) 1463 defer r1() 1464 r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) 1465 defer r2() 1466 1467 for _, mode := range []string{"recover", "run"} { 1468 s.freshStateWithBaseAndMode(c, "core20", mode) 1469 1470 mockServer := makeMockServer(c, &seqRepairs, false) 1471 defer mockServer.Close() 1472 1473 runner := repair.NewRunner() 1474 runner.BaseURL = mustParseURL(mockServer.URL) 1475 runner.LoadState() 1476 1477 rpr, err := runner.Next("canonical") 1478 c.Assert(err, IsNil) 1479 1480 err = rpr.Run() 1481 c.Assert(err, IsNil) 1482 c.Check(filepath.Join(dirs.SnapRepairRunDir, "canonical", "1", "r0.script"), testutil.FileEquals, `#!/bin/sh 1483 env | grep SNAP_SYSTEM_MODE 1484 echo "done" >&$SNAP_REPAIR_STATUS_FD 1485 exit 0`) 1486 1487 c.Check(filepath.Join(dirs.SnapRepairRunDir, "canonical", "1", "r0.done"), testutil.FileEquals, fmt.Sprintf(`repair: canonical-1 1488 revision: 0 1489 summary: repair one 1490 output: 1491 SNAP_SYSTEM_MODE=%s 1492 `, mode)) 1493 // ensure correct permissions 1494 fi, err := os.Stat(filepath.Join(dirs.SnapRepairRunDir, "canonical", "1", "r0.done")) 1495 c.Assert(err, IsNil) 1496 c.Check(fi.Mode(), Equals, os.FileMode(0600)) 1497 } 1498 } 1499 1500 func (s *runnerSuite) TestRepairModesAndBases(c *C) { 1501 repairTempl := `type: repair 1502 authority-id: canonical 1503 brand-id: canonical 1504 repair-id: 1 1505 summary: uc20 recovery repair 1506 timestamp: 2017-07-03T12:00:00Z 1507 body-length: 17 1508 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 1509 %[1]s 1510 #!/bin/sh 1511 exit 0 1512 1513 1514 AXNpZw== 1515 ` 1516 1517 r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) 1518 defer r1() 1519 r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) 1520 defer r2() 1521 1522 tt := []struct { 1523 device *dev 1524 modes []string 1525 bases []string 1526 shouldRun bool 1527 comment string 1528 }{ 1529 // uc20 recover mode assertion 1530 { 1531 &dev{"core20", "recover"}, 1532 []string{"recover"}, 1533 []string{"core20"}, 1534 true, 1535 "uc20 recover mode w/ uc20 recover mode assertion", 1536 }, 1537 { 1538 &dev{"core20", "run"}, 1539 []string{"recover"}, 1540 []string{"core20"}, 1541 false, 1542 "uc20 run mode w/ uc20 recover mode assertion", 1543 }, 1544 { 1545 &dev{base: "core"}, 1546 []string{"recover"}, 1547 []string{"core20"}, 1548 false, 1549 "uc16 w/ uc20 recover mode assertion", 1550 }, 1551 { 1552 &dev{base: "core18"}, 1553 []string{"recover"}, 1554 []string{"core20"}, 1555 false, 1556 "uc18 w/ uc20 recover mode assertion", 1557 }, 1558 1559 // uc20 run mode assertion 1560 { 1561 &dev{"core20", "recover"}, 1562 []string{"run"}, 1563 []string{"core20"}, 1564 false, 1565 "uc20 recover mode w/ uc20 run mode assertion", 1566 }, 1567 { 1568 &dev{"core20", "run"}, 1569 []string{"run"}, 1570 []string{"core20"}, 1571 true, 1572 "uc20 run mode w/ uc20 run mode assertion", 1573 }, 1574 { 1575 &dev{base: "core"}, 1576 []string{"run"}, 1577 []string{"core20"}, 1578 false, 1579 "uc16 w/ uc20 run mode assertion", 1580 }, 1581 { 1582 &dev{base: "core18"}, 1583 []string{"run"}, 1584 []string{"core20"}, 1585 false, 1586 "uc18 w/ uc20 run mode assertion", 1587 }, 1588 1589 // all uc20 modes assertion 1590 { 1591 &dev{"core20", "recover"}, 1592 []string{"run", "recover"}, 1593 []string{"core20"}, 1594 true, 1595 "uc20 recover mode w/ all uc20 modes assertion", 1596 }, 1597 { 1598 &dev{"core20", "run"}, 1599 []string{"run", "recover"}, 1600 []string{"core20"}, 1601 true, 1602 "uc20 run mode w/ all uc20 modes assertion", 1603 }, 1604 { 1605 &dev{base: "core"}, 1606 []string{"run", "recover"}, 1607 []string{"core20"}, 1608 false, 1609 "uc16 w/ all uc20 modes assertion", 1610 }, 1611 { 1612 &dev{base: "core18"}, 1613 []string{"run", "recover"}, 1614 []string{"core20"}, 1615 false, 1616 "uc18 w/ all uc20 modes assertion", 1617 }, 1618 1619 // alternate uc20 run mode only assertion 1620 { 1621 &dev{"core20", "recover"}, 1622 []string{}, 1623 []string{"core20"}, 1624 false, 1625 "uc20 recover mode w/ alternate uc20 run mode assertion", 1626 }, 1627 { 1628 &dev{"core20", "run"}, 1629 []string{}, 1630 []string{"core20"}, 1631 true, 1632 "uc20 run mode w/ alternate uc20 run mode assertion", 1633 }, 1634 { 1635 &dev{base: "core"}, 1636 []string{}, 1637 []string{"core20"}, 1638 false, 1639 "uc16 w/ alternate uc20 run mode assertion", 1640 }, 1641 { 1642 &dev{base: "core18"}, 1643 []string{}, 1644 []string{"core20"}, 1645 false, 1646 "uc18 w/ alternate uc20 run mode assertion", 1647 }, 1648 { 1649 &dev{base: "core"}, 1650 []string{"run"}, 1651 []string{}, 1652 false, 1653 "uc16 w/ uc20 run mode assertion", 1654 }, 1655 { 1656 &dev{base: "core18"}, 1657 []string{"run"}, 1658 []string{}, 1659 false, 1660 "uc16 w/ uc20 run mode assertion", 1661 }, 1662 1663 // all except uc20 recover mode assertion 1664 { 1665 &dev{"core20", "recover"}, 1666 []string{}, 1667 []string{}, 1668 false, 1669 "uc20 recover mode w/ all except uc20 recover mode assertion", 1670 }, 1671 { 1672 &dev{"core20", "run"}, 1673 []string{}, 1674 []string{}, 1675 true, 1676 "uc20 run mode w/ all except uc20 recover mode assertion", 1677 }, 1678 { 1679 &dev{base: "core"}, 1680 []string{}, 1681 []string{}, 1682 true, 1683 "uc16 w/ all except uc20 recover mode assertion", 1684 }, 1685 { 1686 &dev{base: "core18"}, 1687 []string{}, 1688 []string{}, 1689 true, 1690 "uc18 w/ all except uc20 recover mode assertion", 1691 }, 1692 1693 // uc16 and uc18 assertion 1694 { 1695 &dev{"core20", "recover"}, 1696 []string{}, 1697 []string{"core", "core18"}, 1698 false, 1699 "uc20 recover mode w/ uc16 and uc18 assertion", 1700 }, 1701 { 1702 &dev{"core20", "run"}, 1703 []string{}, 1704 []string{"core", "core18"}, 1705 false, 1706 "uc20 run mode w/ uc16 and uc18 assertion", 1707 }, 1708 { 1709 &dev{base: "core"}, 1710 []string{}, 1711 []string{"core", "core18"}, 1712 true, 1713 "uc16 w/ uc16 and uc18 assertion", 1714 }, 1715 { 1716 &dev{base: "core18"}, 1717 []string{}, 1718 []string{"core", "core18"}, 1719 true, 1720 "uc18 w/ uc16 and uc18 assertion", 1721 }, 1722 1723 // just uc16 assertion 1724 { 1725 &dev{"core20", "recover"}, 1726 []string{}, 1727 []string{"core"}, 1728 false, 1729 "uc20 recover mode w/ just uc16 assertion", 1730 }, 1731 { 1732 &dev{"core20", "run"}, 1733 []string{}, 1734 []string{"core"}, 1735 false, 1736 "uc20 run mode w/ just uc16 assertion", 1737 }, 1738 { 1739 &dev{base: "core"}, 1740 []string{}, 1741 []string{"core"}, 1742 true, 1743 "uc16 w/ just uc16 assertion", 1744 }, 1745 { 1746 &dev{base: "core18"}, 1747 []string{}, 1748 []string{"core"}, 1749 false, 1750 "uc18 w/ just uc16 assertion", 1751 }, 1752 1753 // just uc18 assertion 1754 { 1755 &dev{"core20", "recover"}, 1756 []string{}, 1757 []string{"core18"}, 1758 false, 1759 "uc20 recover mode w/ just uc18 assertion", 1760 }, 1761 { 1762 &dev{"core20", "run"}, 1763 []string{}, 1764 []string{"core18"}, 1765 false, 1766 "uc20 run mode w/ just uc18 assertion", 1767 }, 1768 { 1769 &dev{base: "core"}, 1770 []string{}, 1771 []string{"core18"}, 1772 false, 1773 "uc16 w/ just uc18 assertion", 1774 }, 1775 { 1776 &dev{base: "core18"}, 1777 []string{}, 1778 []string{"core18"}, 1779 true, 1780 "uc18 w/ just uc18 assertion", 1781 }, 1782 } 1783 for _, t := range tt { 1784 comment := Commentf(t.comment) 1785 cleanups := []func(){} 1786 1787 // generate the assertion with the bases and modes 1788 basesStr := "" 1789 if len(t.bases) != 0 { 1790 basesStr = "bases:\n" 1791 for _, base := range t.bases { 1792 basesStr += " - " + base + "\n" 1793 } 1794 } 1795 modesStr := "" 1796 if len(t.modes) != 0 { 1797 modesStr = "modes:\n" 1798 for _, mode := range t.modes { 1799 modesStr += " - " + mode + "\n" 1800 } 1801 } 1802 1803 seqRepairs := s.signSeqRepairs(c, []string{fmt.Sprintf(repairTempl, basesStr+modesStr)}) 1804 1805 mockServer := makeMockServer(c, &seqRepairs, false) 1806 cleanups = append(cleanups, func() { mockServer.Close() }) 1807 1808 if t.device == nil { 1809 s.freshState(c) 1810 } else { 1811 s.freshStateWithBaseAndMode(c, t.device.base, t.device.mode) 1812 } 1813 1814 runner := repair.NewRunner() 1815 runner.BaseURL = mustParseURL(mockServer.URL) 1816 runner.LoadState() 1817 1818 script := filepath.Join(dirs.SnapRepairRunDir, "canonical", "1", "r0.script") 1819 1820 rpr, err := runner.Next("canonical") 1821 if t.shouldRun { 1822 c.Assert(err, IsNil, comment) 1823 1824 // run the repair and make sure the script is there 1825 err = rpr.Run() 1826 c.Assert(err, IsNil, comment) 1827 c.Check(script, testutil.FileEquals, "#!/bin/sh\nexit 0\n", comment) 1828 1829 // remove the script for the next iteration 1830 cleanups = append(cleanups, func() { c.Assert(os.RemoveAll(dirs.SnapRepairRunDir), IsNil) }) 1831 } else { 1832 c.Assert(err, Equals, repair.ErrRepairNotFound, comment) 1833 c.Check(script, testutil.FileAbsent, comment) 1834 } 1835 1836 for _, r := range cleanups { 1837 r() 1838 } 1839 } 1840 } 1841 1842 func makeMockRepair(script string) string { 1843 return fmt.Sprintf(`type: repair 1844 authority-id: canonical 1845 brand-id: canonical 1846 repair-id: 1 1847 summary: repair one 1848 series: 1849 - 16 1850 timestamp: 2017-07-02T12:00:00Z 1851 body-length: %d 1852 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 1853 1854 %s 1855 1856 AXNpZw==`, len(script), script) 1857 } 1858 1859 func verifyRepairStatus(c *C, status repair.RepairStatus) { 1860 c.Check(dirs.SnapRepairStateFile, testutil.FileContains, fmt.Sprintf(`{"device":{"brand":"","model":"","base":"","mode":""},"sequences":{"canonical":[{"sequence":1,"revision":0,"status":%d}`, status)) 1861 } 1862 1863 // tests related to correct execution of script 1864 type runScriptSuite struct { 1865 baseRunnerSuite 1866 1867 seqRepairs []string 1868 1869 mockServer *httptest.Server 1870 runner *repair.Runner 1871 1872 runDir string 1873 1874 restoreErrTrackerReportRepair func() 1875 errReport struct { 1876 repair string 1877 errMsg string 1878 dupSig string 1879 extra map[string]string 1880 } 1881 } 1882 1883 var _ = Suite(&runScriptSuite{}) 1884 1885 func (s *runScriptSuite) SetUpTest(c *C) { 1886 s.baseRunnerSuite.SetUpTest(c) 1887 1888 s.mockServer = makeMockServer(c, &s.seqRepairs, false) 1889 s.AddCleanup(func() { s.mockServer.Close() }) 1890 1891 s.runner = repair.NewRunner() 1892 s.runner.BaseURL = mustParseURL(s.mockServer.URL) 1893 s.runner.LoadState() 1894 1895 s.runDir = filepath.Join(dirs.SnapRepairRunDir, "canonical", "1") 1896 1897 restoreErrTrackerReportRepair := repair.MockErrtrackerReportRepair(s.errtrackerReportRepair) 1898 s.AddCleanup(restoreErrTrackerReportRepair) 1899 } 1900 1901 func (s *runScriptSuite) errtrackerReportRepair(repair, errMsg, dupSig string, extra map[string]string) (string, error) { 1902 s.errReport.repair = repair 1903 s.errReport.errMsg = errMsg 1904 s.errReport.dupSig = dupSig 1905 s.errReport.extra = extra 1906 1907 return "some-oops-id", nil 1908 } 1909 1910 func (s *runScriptSuite) testScriptRun(c *C, mockScript string) *repair.Repair { 1911 r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) 1912 defer r1() 1913 r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) 1914 defer r2() 1915 1916 s.seqRepairs = s.signSeqRepairs(c, s.seqRepairs) 1917 1918 rpr, err := s.runner.Next("canonical") 1919 c.Assert(err, IsNil) 1920 1921 err = rpr.Run() 1922 c.Assert(err, IsNil) 1923 1924 c.Check(filepath.Join(s.runDir, "r0.script"), testutil.FileEquals, mockScript) 1925 1926 return rpr 1927 } 1928 1929 func (s *runScriptSuite) verifyRundir(c *C, names []string) { 1930 dirents, err := ioutil.ReadDir(s.runDir) 1931 c.Assert(err, IsNil) 1932 c.Assert(dirents, HasLen, len(names)) 1933 for i := range dirents { 1934 c.Check(dirents[i].Name(), Matches, names[i]) 1935 } 1936 } 1937 1938 type byMtime []os.FileInfo 1939 1940 func (m byMtime) Len() int { return len(m) } 1941 func (m byMtime) Less(i, j int) bool { return m[i].ModTime().Before(m[j].ModTime()) } 1942 func (m byMtime) Swap(i, j int) { m[i], m[j] = m[j], m[i] } 1943 1944 func (s *runScriptSuite) verifyOutput(c *C, name, expectedOutput string) { 1945 c.Check(filepath.Join(s.runDir, name), testutil.FileEquals, expectedOutput) 1946 // ensure correct permissions 1947 fi, err := os.Stat(filepath.Join(s.runDir, name)) 1948 c.Assert(err, IsNil) 1949 c.Check(fi.Mode(), Equals, os.FileMode(0600)) 1950 } 1951 1952 func (s *runScriptSuite) TestRepairBasicRunHappy(c *C) { 1953 script := `#!/bin/sh 1954 echo "happy output" 1955 echo "done" >&$SNAP_REPAIR_STATUS_FD 1956 exit 0 1957 ` 1958 s.seqRepairs = []string{makeMockRepair(script)} 1959 s.testScriptRun(c, script) 1960 // verify 1961 s.verifyRundir(c, []string{ 1962 `^r0.done$`, 1963 `^r0.script$`, 1964 `^work$`, 1965 }) 1966 s.verifyOutput(c, "r0.done", `repair: canonical-1 1967 revision: 0 1968 summary: repair one 1969 output: 1970 happy output 1971 `) 1972 verifyRepairStatus(c, repair.DoneStatus) 1973 } 1974 1975 func (s *runScriptSuite) TestRepairBasicRunUnhappy(c *C) { 1976 script := `#!/bin/sh 1977 echo "unhappy output" 1978 exit 1 1979 ` 1980 s.seqRepairs = []string{makeMockRepair(script)} 1981 s.testScriptRun(c, script) 1982 // verify 1983 s.verifyRundir(c, []string{ 1984 `^r0.retry$`, 1985 `^r0.script$`, 1986 `^work$`, 1987 }) 1988 s.verifyOutput(c, "r0.retry", `repair: canonical-1 1989 revision: 0 1990 summary: repair one 1991 output: 1992 unhappy output 1993 1994 repair canonical-1 revision 0 failed: exit status 1`) 1995 verifyRepairStatus(c, repair.RetryStatus) 1996 1997 c.Check(s.errReport.repair, Equals, "canonical/1") 1998 c.Check(s.errReport.errMsg, Equals, `repair canonical-1 revision 0 failed: exit status 1`) 1999 c.Check(s.errReport.dupSig, Equals, `canonical/1 2000 repair canonical-1 revision 0 failed: exit status 1 2001 output: 2002 repair: canonical-1 2003 revision: 0 2004 summary: repair one 2005 output: 2006 unhappy output 2007 `) 2008 c.Check(s.errReport.extra, DeepEquals, map[string]string{ 2009 "Revision": "0", 2010 "RepairID": "1", 2011 "BrandID": "canonical", 2012 "Status": "retry", 2013 }) 2014 } 2015 2016 func (s *runScriptSuite) TestRepairBasicSkip(c *C) { 2017 script := `#!/bin/sh 2018 echo "other output" 2019 echo "skip" >&$SNAP_REPAIR_STATUS_FD 2020 exit 0 2021 ` 2022 s.seqRepairs = []string{makeMockRepair(script)} 2023 s.testScriptRun(c, script) 2024 // verify 2025 s.verifyRundir(c, []string{ 2026 `^r0.script$`, 2027 `^r0.skip$`, 2028 `^work$`, 2029 }) 2030 s.verifyOutput(c, "r0.skip", `repair: canonical-1 2031 revision: 0 2032 summary: repair one 2033 output: 2034 other output 2035 `) 2036 verifyRepairStatus(c, repair.SkipStatus) 2037 } 2038 2039 func (s *runScriptSuite) TestRepairBasicRunUnhappyThenHappy(c *C) { 2040 script := `#!/bin/sh 2041 if [ -f zzz-ran-once ]; then 2042 echo "happy now" 2043 echo "done" >&$SNAP_REPAIR_STATUS_FD 2044 exit 0 2045 fi 2046 echo "unhappy output" 2047 touch zzz-ran-once 2048 exit 1 2049 ` 2050 s.seqRepairs = []string{makeMockRepair(script)} 2051 rpr := s.testScriptRun(c, script) 2052 s.verifyRundir(c, []string{ 2053 `^r0.retry$`, 2054 `^r0.script$`, 2055 `^work$`, 2056 }) 2057 s.verifyOutput(c, "r0.retry", `repair: canonical-1 2058 revision: 0 2059 summary: repair one 2060 output: 2061 unhappy output 2062 2063 repair canonical-1 revision 0 failed: exit status 1`) 2064 verifyRepairStatus(c, repair.RetryStatus) 2065 2066 // run again, it will be happy this time 2067 err := rpr.Run() 2068 c.Assert(err, IsNil) 2069 2070 s.verifyRundir(c, []string{ 2071 `^r0.done$`, 2072 `^r0.retry$`, 2073 `^r0.script$`, 2074 `^work$`, 2075 }) 2076 s.verifyOutput(c, "r0.done", `repair: canonical-1 2077 revision: 0 2078 summary: repair one 2079 output: 2080 happy now 2081 `) 2082 verifyRepairStatus(c, repair.DoneStatus) 2083 } 2084 2085 func (s *runScriptSuite) TestRepairHitsTimeout(c *C) { 2086 r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) 2087 defer r1() 2088 r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) 2089 defer r2() 2090 2091 restore := repair.MockDefaultRepairTimeout(100 * time.Millisecond) 2092 defer restore() 2093 2094 script := `#!/bin/sh 2095 echo "output before timeout" 2096 sleep 100 2097 ` 2098 s.seqRepairs = []string{makeMockRepair(script)} 2099 s.seqRepairs = s.signSeqRepairs(c, s.seqRepairs) 2100 2101 rpr, err := s.runner.Next("canonical") 2102 c.Assert(err, IsNil) 2103 2104 err = rpr.Run() 2105 c.Assert(err, IsNil) 2106 2107 s.verifyRundir(c, []string{ 2108 `^r0.retry$`, 2109 `^r0.script$`, 2110 `^work$`, 2111 }) 2112 s.verifyOutput(c, "r0.retry", `repair: canonical-1 2113 revision: 0 2114 summary: repair one 2115 output: 2116 output before timeout 2117 2118 repair canonical-1 revision 0 failed: repair did not finish within 100ms`) 2119 verifyRepairStatus(c, repair.RetryStatus) 2120 } 2121 2122 func (s *runScriptSuite) TestRepairHasCorrectPath(c *C) { 2123 r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) 2124 defer r1() 2125 r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) 2126 defer r2() 2127 2128 script := `#!/bin/sh 2129 echo PATH=$PATH 2130 ls -l ${PATH##*:}/repair 2131 ` 2132 s.seqRepairs = []string{makeMockRepair(script)} 2133 s.seqRepairs = s.signSeqRepairs(c, s.seqRepairs) 2134 2135 rpr, err := s.runner.Next("canonical") 2136 c.Assert(err, IsNil) 2137 2138 err = rpr.Run() 2139 c.Assert(err, IsNil) 2140 2141 c.Check(filepath.Join(s.runDir, "r0.retry"), testutil.FileMatches, fmt.Sprintf(`(?ms).*^PATH=.*:.*/run/snapd/repair/tools.*`)) 2142 c.Check(filepath.Join(s.runDir, "r0.retry"), testutil.FileContains, `/repair -> /usr/lib/snapd/snap-repair`) 2143 2144 // run again and ensure no error happens 2145 err = rpr.Run() 2146 c.Assert(err, IsNil) 2147 2148 } 2149 2150 // shared1620RunnerSuite is embedded by runner16Suite and 2151 // runner20Suite and the tests are run once with a simulated uc16 and 2152 // once with a simulated uc20 environment 2153 type shared1620RunnerSuite struct { 2154 baseRunnerSuite 2155 2156 writeSeedAssert func(c *C, fname string, a asserts.Assertion) 2157 2158 // this is so we can check device details that will be different in the 2159 // 20 version of tests from the 16 version of tests 2160 expBase string 2161 expMode string 2162 } 2163 2164 func (s *shared1620RunnerSuite) TestTLSTime(c *C) { 2165 s.freshState(c) 2166 runner := repair.NewRunner() 2167 err := runner.LoadState() 2168 c.Assert(err, IsNil) 2169 epoch := time.Unix(0, 0) 2170 r := repair.MockTimeNow(func() time.Time { 2171 return epoch 2172 }) 2173 defer r() 2174 c.Check(runner.TLSTime().Equal(s.seedTime), Equals, true) 2175 } 2176 2177 func (s *shared1620RunnerSuite) TestLoadStateInitState(c *C) { 2178 // sanity 2179 c.Check(osutil.IsDirectory(dirs.SnapRepairDir), Equals, false) 2180 c.Check(osutil.FileExists(dirs.SnapRepairStateFile), Equals, false) 2181 // setup realistic seed/assertions 2182 r := sysdb.InjectTrusted(s.storeSigning.Trusted) 2183 defer r() 2184 s.writeSeedAssert(c, "store.account-key", s.storeSigning.StoreAccountKey("")) 2185 s.writeSeedAssert(c, "brand.account", s.brandAcct) 2186 s.writeSeedAssert(c, "brand.account-key", s.brandAcctKey) 2187 s.writeSeedAssert(c, "model", s.modelAs) 2188 2189 runner := repair.NewRunner() 2190 err := runner.LoadState() 2191 c.Assert(err, IsNil) 2192 c.Check(osutil.FileExists(dirs.SnapRepairStateFile), Equals, true) 2193 2194 brand, model := runner.BrandModel() 2195 c.Check(brand, Equals, "my-brand") 2196 c.Check(model, Equals, "my-model-2") 2197 2198 base, mode := runner.BaseMode() 2199 c.Check(base, Equals, s.expBase) 2200 c.Check(mode, Equals, s.expMode) 2201 2202 c.Check(runner.TimeLowerBound().Equal(s.seedTime), Equals, true) 2203 } 2204 2205 type runner16Suite struct { 2206 shared1620RunnerSuite 2207 } 2208 2209 var _ = Suite(&runner16Suite{}) 2210 2211 func (s *runner16Suite) SetUpTest(c *C) { 2212 s.shared1620RunnerSuite.SetUpTest(c) 2213 2214 s.shared1620RunnerSuite.expBase = "core" 2215 s.shared1620RunnerSuite.expMode = "" 2216 2217 s.seedAssertsDir = filepath.Join(dirs.SnapSeedDir, "assertions") 2218 2219 // dummy seed yaml 2220 err := os.MkdirAll(s.seedAssertsDir, 0755) 2221 c.Assert(err, IsNil) 2222 seedYamlFn := filepath.Join(dirs.SnapSeedDir, "seed.yaml") 2223 err = ioutil.WriteFile(seedYamlFn, nil, 0644) 2224 c.Assert(err, IsNil) 2225 seedTime, err := time.Parse(time.RFC3339, "2017-08-11T15:49:49Z") 2226 c.Assert(err, IsNil) 2227 err = os.Chtimes(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), seedTime, seedTime) 2228 c.Assert(err, IsNil) 2229 s.seedTime = seedTime 2230 2231 s.t0 = time.Now().UTC().Truncate(time.Minute) 2232 2233 s.writeSeedAssert = s.writeSeedAssert16 2234 } 2235 2236 func (s *runner16Suite) writeSeedAssert16(c *C, fname string, a asserts.Assertion) { 2237 err := ioutil.WriteFile(filepath.Join(s.seedAssertsDir, fname), asserts.Encode(a), 0644) 2238 c.Assert(err, IsNil) 2239 } 2240 2241 func (s *runner16Suite) rmSeedAssert16(c *C, fname string) { 2242 err := os.Remove(filepath.Join(s.seedAssertsDir, fname)) 2243 c.Assert(err, IsNil) 2244 } 2245 2246 func (s *runner16Suite) TestLoadStateInitDeviceInfoFail(c *C) { 2247 // sanity 2248 c.Check(osutil.IsDirectory(dirs.SnapRepairDir), Equals, false) 2249 c.Check(osutil.FileExists(dirs.SnapRepairStateFile), Equals, false) 2250 // setup realistic seed/assertions 2251 r := sysdb.InjectTrusted(s.storeSigning.Trusted) 2252 defer r() 2253 2254 const errPrefix = "cannot set device information: " 2255 tests := []struct { 2256 breakFunc func() 2257 expectedErr string 2258 }{ 2259 {func() { s.rmSeedAssert16(c, "model") }, errPrefix + "no model assertion in seed data"}, 2260 {func() { s.rmSeedAssert16(c, "brand.account") }, errPrefix + "no brand account assertion in seed data"}, 2261 {func() { s.rmSeedAssert16(c, "brand.account-key") }, errPrefix + `cannot find public key.*`}, 2262 {func() { 2263 // broken signature 2264 blob := asserts.Encode(s.brandAcct) 2265 err := ioutil.WriteFile(filepath.Join(s.seedAssertsDir, "brand.account"), blob[:len(blob)-3], 0644) 2266 c.Assert(err, IsNil) 2267 }, errPrefix + "cannot decode signature:.*"}, 2268 {func() { s.writeSeedAssert(c, "model2", s.modelAs) }, errPrefix + "multiple models in seed assertions"}, 2269 } 2270 2271 for _, test := range tests { 2272 s.writeSeedAssert(c, "store.account-key", s.storeSigning.StoreAccountKey("")) 2273 s.writeSeedAssert(c, "brand.account", s.brandAcct) 2274 s.writeSeedAssert(c, "brand.account-key", s.brandAcctKey) 2275 s.writeSeedAssert(c, "model", s.modelAs) 2276 2277 test.breakFunc() 2278 2279 runner := repair.NewRunner() 2280 err := runner.LoadState() 2281 c.Check(err, ErrorMatches, test.expectedErr) 2282 } 2283 } 2284 2285 type runner20Suite struct { 2286 shared1620RunnerSuite 2287 } 2288 2289 var _ = Suite(&runner20Suite{}) 2290 2291 var mockModeenv = []byte(` 2292 mode=run 2293 model=my-brand/my-model-2 2294 base=core20_1.snap 2295 `) 2296 2297 func (s *runner20Suite) SetUpTest(c *C) { 2298 s.shared1620RunnerSuite.SetUpTest(c) 2299 2300 s.shared1620RunnerSuite.expBase = "core20" 2301 s.shared1620RunnerSuite.expMode = "run" 2302 2303 s.seedAssertsDir = filepath.Join(dirs.SnapSeedDir, "/systems/20201212/assertions") 2304 err := os.MkdirAll(s.seedAssertsDir, 0755) 2305 c.Assert(err, IsNil) 2306 2307 // write dummy modeenv 2308 err = os.MkdirAll(filepath.Dir(dirs.SnapModeenvFile), 0755) 2309 c.Assert(err, IsNil) 2310 err = ioutil.WriteFile(dirs.SnapModeenvFile, mockModeenv, 0644) 2311 c.Assert(err, IsNil) 2312 // validate that modeenv is actually valid 2313 _, err = boot.ReadModeenv("") 2314 c.Assert(err, IsNil) 2315 2316 seedTime, err := time.Parse(time.RFC3339, "2017-08-11T15:49:49Z") 2317 c.Assert(err, IsNil) 2318 err = os.Chtimes(dirs.SnapModeenvFile, seedTime, seedTime) 2319 c.Assert(err, IsNil) 2320 s.seedTime = seedTime 2321 s.t0 = time.Now().UTC().Truncate(time.Minute) 2322 2323 s.writeSeedAssert = s.writeSeedAssert20 2324 } 2325 2326 func (s *runner20Suite) writeSeedAssert20(c *C, fname string, a asserts.Assertion) { 2327 var fn string 2328 if _, ok := a.(*asserts.Model); ok { 2329 fn = filepath.Join(s.seedAssertsDir, "../model") 2330 } else { 2331 fn = filepath.Join(s.seedAssertsDir, fname) 2332 } 2333 err := ioutil.WriteFile(fn, asserts.Encode(a), 0644) 2334 c.Assert(err, IsNil) 2335 2336 // ensure model assertion file has the correct seed time 2337 if _, ok := a.(*asserts.Model); ok { 2338 err = os.Chtimes(fn, s.seedTime, s.seedTime) 2339 c.Assert(err, IsNil) 2340 } 2341 } 2342 2343 func (s *runner20Suite) TestLoadStateInitDeviceInfoModeenvInvalidContent(c *C) { 2344 runner := repair.NewRunner() 2345 2346 for _, tc := range []struct { 2347 modelStr string 2348 expectedErr string 2349 }{ 2350 { 2351 `invalid-key-value`, 2352 "cannot set device information: No option model in section ", 2353 }, { 2354 `model=`, 2355 `cannot set device information: cannot find brand/model in modeenv model string ""`, 2356 }, { 2357 `model=brand-but-no-model`, 2358 `cannot set device information: cannot find brand/model in modeenv model string "brand-but-no-model"`, 2359 }, 2360 } { 2361 err := ioutil.WriteFile(dirs.SnapModeenvFile, []byte(tc.modelStr), 0644) 2362 c.Assert(err, IsNil) 2363 err = runner.LoadState() 2364 c.Check(err, ErrorMatches, tc.expectedErr) 2365 } 2366 } 2367 2368 func (s *runner20Suite) TestLoadStateInitDeviceInfoModeenvIncorrectPermissions(c *C) { 2369 runner := repair.NewRunner() 2370 2371 err := os.Chmod(dirs.SnapModeenvFile, 0300) 2372 c.Assert(err, IsNil) 2373 s.AddCleanup(func() { 2374 err := os.Chmod(dirs.SnapModeenvFile, 0644) 2375 c.Assert(err, IsNil) 2376 }) 2377 err = runner.LoadState() 2378 c.Check(err, ErrorMatches, "cannot set device information: open /.*/modeenv: permission denied") 2379 }