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