github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/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 const freshStateJSON = `{"device":{"brand":"my-brand","model":"my-model"},"time-lower-bound":"2017-08-11T15:49:49Z"}` 158 159 func (s *baseRunnerSuite) freshState(c *C) { 160 err := os.MkdirAll(dirs.SnapRepairDir, 0775) 161 c.Assert(err, IsNil) 162 err = ioutil.WriteFile(dirs.SnapRepairStateFile, []byte(freshStateJSON), 0600) 163 c.Assert(err, IsNil) 164 } 165 166 type runnerSuite struct { 167 baseRunnerSuite 168 169 restore func() 170 } 171 172 func (s *runnerSuite) SetUpSuite(c *C) { 173 s.baseRunnerSuite.SetUpSuite(c) 174 s.restore = snapdenv.SetUserAgentFromVersion("1", nil, "snap-repair") 175 } 176 177 func (s *runnerSuite) TearDownSuite(c *C) { 178 s.restore() 179 } 180 181 var _ = Suite(&runnerSuite{}) 182 183 var ( 184 testKey = `type: account-key 185 authority-id: canonical 186 account-id: canonical 187 name: repair 188 public-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 189 since: 2015-11-16T15:04:00Z 190 body-length: 149 191 sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij 192 193 AcZrBFaFwYABAvCX5A8dTcdLdhdiuy2YRHO5CAfM5InQefkKOhNMUq2yfi3Sk6trUHxskhZkPnm4 194 NKx2yRr332q7AJXQHLX+DrZ29ycyoQ2NQGO3eAfQ0hjAAQFYBF8SSh5SutPu5XCVABEBAAE= 195 196 AXNpZw== 197 ` 198 199 testRepair = `type: repair 200 authority-id: canonical 201 brand-id: canonical 202 repair-id: 2 203 summary: repair two 204 architectures: 205 - amd64 206 - arm64 207 series: 208 - 16 209 models: 210 - xyz/frobinator 211 timestamp: 2017-03-30T12:22:16Z 212 body-length: 7 213 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 214 215 script 216 217 218 AXNpZw== 219 ` 220 testHeadersResp = `{"headers": 221 {"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"}}` 222 ) 223 224 func mustParseURL(s string) *url.URL { 225 u, err := url.Parse(s) 226 if err != nil { 227 panic(err) 228 } 229 return u 230 } 231 232 func (s *runnerSuite) mockBrokenTimeNowSetToEpoch(c *C, runner *repair.Runner) (restore func()) { 233 epoch := time.Unix(0, 0) 234 r := repair.MockTimeNow(func() time.Time { 235 return epoch 236 }) 237 c.Check(runner.TLSTime().Equal(epoch), Equals, true) 238 return r 239 } 240 241 func (s *runnerSuite) checkBrokenTimeNowMitigated(c *C, runner *repair.Runner) { 242 c.Check(runner.TLSTime().Before(s.t0), Equals, false) 243 } 244 245 func (s *runnerSuite) TestFetchJustRepair(c *C) { 246 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 247 ua := r.Header.Get("User-Agent") 248 c.Check(strings.Contains(ua, "snap-repair"), Equals, true) 249 c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") 250 c.Check(r.URL.Path, Equals, "/repairs/canonical/2") 251 io.WriteString(w, testRepair) 252 })) 253 254 c.Assert(mockServer, NotNil) 255 defer mockServer.Close() 256 257 runner := repair.NewRunner() 258 runner.BaseURL = mustParseURL(mockServer.URL) 259 260 r := s.mockBrokenTimeNowSetToEpoch(c, runner) 261 defer r() 262 263 repair, aux, err := runner.Fetch("canonical", 2, -1) 264 c.Assert(err, IsNil) 265 c.Check(repair, NotNil) 266 c.Check(aux, HasLen, 0) 267 c.Check(repair.BrandID(), Equals, "canonical") 268 c.Check(repair.RepairID(), Equals, 2) 269 c.Check(repair.Body(), DeepEquals, []byte("script\n")) 270 271 s.checkBrokenTimeNowMitigated(c, runner) 272 } 273 274 func (s *runnerSuite) TestFetchScriptTooBig(c *C) { 275 restore := repair.MockMaxRepairScriptSize(4) 276 defer restore() 277 278 n := 0 279 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 280 n++ 281 c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") 282 c.Check(r.URL.Path, Equals, "/repairs/canonical/2") 283 io.WriteString(w, testRepair) 284 })) 285 286 c.Assert(mockServer, NotNil) 287 defer mockServer.Close() 288 289 runner := repair.NewRunner() 290 runner.BaseURL = mustParseURL(mockServer.URL) 291 292 _, _, err := runner.Fetch("canonical", 2, -1) 293 c.Assert(err, ErrorMatches, `assertion body length 7 exceeds maximum body size 4 for "repair".*`) 294 c.Assert(n, Equals, 1) 295 } 296 297 var ( 298 testRetryStrategy = retry.LimitCount(5, retry.LimitTime(1*time.Second, 299 retry.Exponential{ 300 Initial: 1 * time.Millisecond, 301 Factor: 1, 302 }, 303 )) 304 ) 305 306 func (s *runnerSuite) TestFetch500(c *C) { 307 restore := repair.MockFetchRetryStrategy(testRetryStrategy) 308 defer restore() 309 310 n := 0 311 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 312 n++ 313 w.WriteHeader(500) 314 })) 315 316 c.Assert(mockServer, NotNil) 317 defer mockServer.Close() 318 319 runner := repair.NewRunner() 320 runner.BaseURL = mustParseURL(mockServer.URL) 321 322 _, _, err := runner.Fetch("canonical", 2, -1) 323 c.Assert(err, ErrorMatches, "cannot fetch repair, unexpected status 500") 324 c.Assert(n, Equals, 5) 325 } 326 327 func (s *runnerSuite) TestFetchEmpty(c *C) { 328 restore := repair.MockFetchRetryStrategy(testRetryStrategy) 329 defer restore() 330 331 n := 0 332 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 333 n++ 334 w.WriteHeader(200) 335 })) 336 337 c.Assert(mockServer, NotNil) 338 defer mockServer.Close() 339 340 runner := repair.NewRunner() 341 runner.BaseURL = mustParseURL(mockServer.URL) 342 343 _, _, err := runner.Fetch("canonical", 2, -1) 344 c.Assert(err, Equals, io.ErrUnexpectedEOF) 345 c.Assert(n, Equals, 5) 346 } 347 348 func (s *runnerSuite) TestFetchBroken(c *C) { 349 restore := repair.MockFetchRetryStrategy(testRetryStrategy) 350 defer restore() 351 352 n := 0 353 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 354 n++ 355 w.WriteHeader(200) 356 io.WriteString(w, "xyz:") 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) TestFetchNotFound(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(404) 378 })) 379 380 c.Assert(mockServer, NotNil) 381 defer mockServer.Close() 382 383 runner := repair.NewRunner() 384 runner.BaseURL = mustParseURL(mockServer.URL) 385 386 r := s.mockBrokenTimeNowSetToEpoch(c, runner) 387 defer r() 388 389 _, _, err := runner.Fetch("canonical", 2, -1) 390 c.Assert(err, Equals, repair.ErrRepairNotFound) 391 c.Assert(n, Equals, 1) 392 393 s.checkBrokenTimeNowMitigated(c, runner) 394 } 395 396 func (s *runnerSuite) TestFetchIfNoneMatchNotModified(c *C) { 397 restore := repair.MockFetchRetryStrategy(testRetryStrategy) 398 defer restore() 399 400 n := 0 401 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 402 n++ 403 c.Check(r.Header.Get("If-None-Match"), Equals, `"0"`) 404 w.WriteHeader(304) 405 })) 406 407 c.Assert(mockServer, NotNil) 408 defer mockServer.Close() 409 410 runner := repair.NewRunner() 411 runner.BaseURL = mustParseURL(mockServer.URL) 412 413 r := s.mockBrokenTimeNowSetToEpoch(c, runner) 414 defer r() 415 416 _, _, err := runner.Fetch("canonical", 2, 0) 417 c.Assert(err, Equals, repair.ErrRepairNotModified) 418 c.Assert(n, Equals, 1) 419 420 s.checkBrokenTimeNowMitigated(c, runner) 421 } 422 423 func (s *runnerSuite) TestFetchIgnoreSupersededRevision(c *C) { 424 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 425 io.WriteString(w, testRepair) 426 })) 427 428 c.Assert(mockServer, NotNil) 429 defer mockServer.Close() 430 431 runner := repair.NewRunner() 432 runner.BaseURL = mustParseURL(mockServer.URL) 433 434 _, _, err := runner.Fetch("canonical", 2, 2) 435 c.Assert(err, Equals, repair.ErrRepairNotModified) 436 } 437 438 func (s *runnerSuite) TestFetchIdMismatch(c *C) { 439 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 440 c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") 441 io.WriteString(w, testRepair) 442 })) 443 444 c.Assert(mockServer, NotNil) 445 defer mockServer.Close() 446 447 runner := repair.NewRunner() 448 runner.BaseURL = mustParseURL(mockServer.URL) 449 450 _, _, err := runner.Fetch("canonical", 4, -1) 451 c.Assert(err, ErrorMatches, `cannot fetch repair, repair id mismatch canonical/2 != canonical/4`) 452 } 453 454 func (s *runnerSuite) TestFetchWrongFirstType(c *C) { 455 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 456 c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") 457 c.Check(r.URL.Path, Equals, "/repairs/canonical/2") 458 io.WriteString(w, testKey) 459 })) 460 461 c.Assert(mockServer, NotNil) 462 defer mockServer.Close() 463 464 runner := repair.NewRunner() 465 runner.BaseURL = mustParseURL(mockServer.URL) 466 467 _, _, err := runner.Fetch("canonical", 2, -1) 468 c.Assert(err, ErrorMatches, `cannot fetch repair, unexpected first assertion "account-key"`) 469 } 470 471 func (s *runnerSuite) TestFetchRepairPlusKey(c *C) { 472 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 473 c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") 474 c.Check(r.URL.Path, Equals, "/repairs/canonical/2") 475 io.WriteString(w, testRepair) 476 io.WriteString(w, "\n") 477 io.WriteString(w, testKey) 478 })) 479 480 c.Assert(mockServer, NotNil) 481 defer mockServer.Close() 482 483 runner := repair.NewRunner() 484 runner.BaseURL = mustParseURL(mockServer.URL) 485 486 repair, aux, err := runner.Fetch("canonical", 2, -1) 487 c.Assert(err, IsNil) 488 c.Check(repair, NotNil) 489 c.Check(aux, HasLen, 1) 490 _, ok := aux[0].(*asserts.AccountKey) 491 c.Check(ok, Equals, true) 492 } 493 494 func (s *runnerSuite) TestPeek(c *C) { 495 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 496 ua := r.Header.Get("User-Agent") 497 c.Check(strings.Contains(ua, "snap-repair"), Equals, true) 498 c.Check(r.Header.Get("Accept"), Equals, "application/json") 499 c.Check(r.URL.Path, Equals, "/repairs/canonical/2") 500 io.WriteString(w, testHeadersResp) 501 })) 502 503 c.Assert(mockServer, NotNil) 504 defer mockServer.Close() 505 506 runner := repair.NewRunner() 507 runner.BaseURL = mustParseURL(mockServer.URL) 508 509 r := s.mockBrokenTimeNowSetToEpoch(c, runner) 510 defer r() 511 512 h, err := runner.Peek("canonical", 2) 513 c.Assert(err, IsNil) 514 c.Check(h["series"], DeepEquals, []interface{}{"16"}) 515 c.Check(h["architectures"], DeepEquals, []interface{}{"amd64", "arm64"}) 516 c.Check(h["models"], DeepEquals, []interface{}{"xyz/frobinator"}) 517 518 s.checkBrokenTimeNowMitigated(c, runner) 519 } 520 521 func (s *runnerSuite) TestPeek500(c *C) { 522 restore := repair.MockPeekRetryStrategy(testRetryStrategy) 523 defer restore() 524 525 n := 0 526 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 527 n++ 528 w.WriteHeader(500) 529 })) 530 531 c.Assert(mockServer, NotNil) 532 defer mockServer.Close() 533 534 runner := repair.NewRunner() 535 runner.BaseURL = mustParseURL(mockServer.URL) 536 537 _, err := runner.Peek("canonical", 2) 538 c.Assert(err, ErrorMatches, "cannot peek repair headers, unexpected status 500") 539 c.Assert(n, Equals, 5) 540 } 541 542 func (s *runnerSuite) TestPeekInvalid(c *C) { 543 restore := repair.MockPeekRetryStrategy(testRetryStrategy) 544 defer restore() 545 546 n := 0 547 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 548 n++ 549 w.WriteHeader(200) 550 io.WriteString(w, "{") 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, Equals, io.ErrUnexpectedEOF) 561 c.Assert(n, Equals, 5) 562 } 563 564 func (s *runnerSuite) TestPeekNotFound(c *C) { 565 n := 0 566 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 567 n++ 568 w.WriteHeader(404) 569 })) 570 571 c.Assert(mockServer, NotNil) 572 defer mockServer.Close() 573 574 runner := repair.NewRunner() 575 runner.BaseURL = mustParseURL(mockServer.URL) 576 577 r := s.mockBrokenTimeNowSetToEpoch(c, runner) 578 defer r() 579 580 _, err := runner.Peek("canonical", 2) 581 c.Assert(err, Equals, repair.ErrRepairNotFound) 582 c.Assert(n, Equals, 1) 583 584 s.checkBrokenTimeNowMitigated(c, runner) 585 } 586 587 func (s *runnerSuite) TestPeekIdMismatch(c *C) { 588 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 589 c.Check(r.Header.Get("Accept"), Equals, "application/json") 590 io.WriteString(w, testHeadersResp) 591 })) 592 593 c.Assert(mockServer, NotNil) 594 defer mockServer.Close() 595 596 runner := repair.NewRunner() 597 runner.BaseURL = mustParseURL(mockServer.URL) 598 599 _, err := runner.Peek("canonical", 4) 600 c.Assert(err, ErrorMatches, `cannot peek repair headers, repair id mismatch canonical/2 != canonical/4`) 601 } 602 603 func (s *runnerSuite) TestLoadState(c *C) { 604 s.freshState(c) 605 606 runner := repair.NewRunner() 607 err := runner.LoadState() 608 c.Assert(err, IsNil) 609 brand, model := runner.BrandModel() 610 c.Check(brand, Equals, "my-brand") 611 c.Check(model, Equals, "my-model") 612 } 613 614 func (s *runnerSuite) TestLoadStateInitStateFail(c *C) { 615 err := os.MkdirAll(dirs.SnapSeedDir, 0755) 616 c.Assert(err, IsNil) 617 618 restore := makeReadOnly(c, filepath.Dir(dirs.SnapSeedDir)) 619 defer restore() 620 621 runner := repair.NewRunner() 622 err = runner.LoadState() 623 c.Check(err, ErrorMatches, `cannot create repair state directory:.*`) 624 } 625 626 func (s *runnerSuite) TestSaveStateFail(c *C) { 627 s.freshState(c) 628 629 runner := repair.NewRunner() 630 err := runner.LoadState() 631 c.Assert(err, IsNil) 632 633 restore := makeReadOnly(c, dirs.SnapRepairDir) 634 defer restore() 635 636 // no error because this is a no-op 637 err = runner.SaveState() 638 c.Check(err, IsNil) 639 640 // mark as modified 641 runner.SetStateModified(true) 642 643 err = runner.SaveState() 644 c.Check(err, ErrorMatches, `cannot save repair state:.*`) 645 } 646 647 func (s *runnerSuite) TestSaveState(c *C) { 648 s.freshState(c) 649 650 runner := repair.NewRunner() 651 err := runner.LoadState() 652 c.Assert(err, IsNil) 653 654 runner.SetSequence("canonical", []*repair.RepairState{ 655 {Sequence: 1, Revision: 3}, 656 }) 657 // mark as modified 658 runner.SetStateModified(true) 659 660 err = runner.SaveState() 661 c.Assert(err, IsNil) 662 663 c.Check(dirs.SnapRepairStateFile, testutil.FileEquals, `{"device":{"brand":"my-brand","model":"my-model"},"sequences":{"canonical":[{"sequence":1,"revision":3,"status":0}]},"time-lower-bound":"2017-08-11T15:49:49Z"}`) 664 } 665 666 func (s *runnerSuite) TestApplicable(c *C) { 667 s.freshState(c) 668 runner := repair.NewRunner() 669 err := runner.LoadState() 670 c.Assert(err, IsNil) 671 672 scenarios := []struct { 673 headers map[string]interface{} 674 applicable bool 675 }{ 676 {nil, true}, 677 {map[string]interface{}{"series": []interface{}{"18"}}, false}, 678 {map[string]interface{}{"series": []interface{}{"18", "16"}}, true}, 679 {map[string]interface{}{"series": "18"}, false}, 680 {map[string]interface{}{"series": []interface{}{18}}, false}, 681 {map[string]interface{}{"architectures": []interface{}{arch.DpkgArchitecture()}}, true}, 682 {map[string]interface{}{"architectures": []interface{}{"other-arch"}}, false}, 683 {map[string]interface{}{"architectures": []interface{}{"other-arch", arch.DpkgArchitecture()}}, true}, 684 {map[string]interface{}{"architectures": arch.DpkgArchitecture()}, false}, 685 {map[string]interface{}{"models": []interface{}{"my-brand/my-model"}}, true}, 686 {map[string]interface{}{"models": []interface{}{"other-brand/other-model"}}, false}, 687 {map[string]interface{}{"models": []interface{}{"other-brand/other-model", "my-brand/my-model"}}, true}, 688 {map[string]interface{}{"models": "my-brand/my-model"}, false}, 689 // model prefix matches 690 {map[string]interface{}{"models": []interface{}{"my-brand/*"}}, true}, 691 {map[string]interface{}{"models": []interface{}{"my-brand/my-mod*"}}, true}, 692 {map[string]interface{}{"models": []interface{}{"my-brand/xxx*"}}, false}, 693 {map[string]interface{}{"models": []interface{}{"my-brand/my-mod*", "my-brand/xxx*"}}, true}, 694 {map[string]interface{}{"models": []interface{}{"my*"}}, false}, 695 {map[string]interface{}{"disabled": "true"}, false}, 696 {map[string]interface{}{"disabled": "false"}, true}, 697 } 698 699 for _, scen := range scenarios { 700 ok := runner.Applicable(scen.headers) 701 c.Check(ok, Equals, scen.applicable, Commentf("%v", scen)) 702 } 703 } 704 705 var ( 706 nextRepairs = []string{`type: repair 707 authority-id: canonical 708 brand-id: canonical 709 repair-id: 1 710 summary: repair one 711 timestamp: 2017-07-01T12:00:00Z 712 body-length: 8 713 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 714 715 scriptA 716 717 718 AXNpZw==`, 719 `type: repair 720 authority-id: canonical 721 brand-id: canonical 722 repair-id: 2 723 summary: repair two 724 series: 725 - 33 726 timestamp: 2017-07-02T12:00:00Z 727 body-length: 8 728 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 729 730 scriptB 731 732 733 AXNpZw==`, 734 `type: repair 735 revision: 2 736 authority-id: canonical 737 brand-id: canonical 738 repair-id: 3 739 summary: repair three rev2 740 series: 741 - 16 742 timestamp: 2017-07-03T12:00:00Z 743 body-length: 8 744 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 745 746 scriptC 747 748 749 AXNpZw== 750 `} 751 752 repair3Rev4 = `type: repair 753 revision: 4 754 authority-id: canonical 755 brand-id: canonical 756 repair-id: 3 757 summary: repair three rev4 758 series: 759 - 16 760 timestamp: 2017-07-03T12:00:00Z 761 body-length: 9 762 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 763 764 scriptC2 765 766 767 AXNpZw== 768 ` 769 770 repair4 = `type: repair 771 authority-id: canonical 772 brand-id: canonical 773 repair-id: 4 774 summary: repair four 775 timestamp: 2017-07-03T12:00:00Z 776 body-length: 8 777 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 778 779 scriptD 780 781 782 AXNpZw== 783 ` 784 ) 785 786 func makeMockServer(c *C, seqRepairs *[]string, redirectFirst bool) *httptest.Server { 787 var mockServer *httptest.Server 788 mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 789 ua := r.Header.Get("User-Agent") 790 c.Check(strings.Contains(ua, "snap-repair"), Equals, true) 791 792 urlPath := r.URL.Path 793 if redirectFirst && r.Header.Get("Accept") == asserts.MediaType { 794 if !strings.HasPrefix(urlPath, "/final/") { 795 // redirect 796 finalURL := mockServer.URL + "/final" + r.URL.Path 797 w.Header().Set("Location", finalURL) 798 w.WriteHeader(302) 799 return 800 } 801 urlPath = strings.TrimPrefix(urlPath, "/final") 802 } 803 804 c.Check(strings.HasPrefix(urlPath, "/repairs/canonical/"), Equals, true) 805 806 seq, err := strconv.Atoi(strings.TrimPrefix(urlPath, "/repairs/canonical/")) 807 c.Assert(err, IsNil) 808 809 if seq > len(*seqRepairs) { 810 w.WriteHeader(404) 811 return 812 } 813 814 rpr := []byte((*seqRepairs)[seq-1]) 815 dec := asserts.NewDecoder(bytes.NewBuffer(rpr)) 816 repair, err := dec.Decode() 817 c.Assert(err, IsNil) 818 819 switch r.Header.Get("Accept") { 820 case "application/json": 821 b, err := json.Marshal(map[string]interface{}{ 822 "headers": repair.Headers(), 823 }) 824 c.Assert(err, IsNil) 825 w.Write(b) 826 case asserts.MediaType: 827 etag := fmt.Sprintf(`"%d"`, repair.Revision()) 828 if strings.Contains(r.Header.Get("If-None-Match"), etag) { 829 w.WriteHeader(304) 830 return 831 } 832 w.Write(rpr) 833 } 834 })) 835 836 c.Assert(mockServer, NotNil) 837 838 return mockServer 839 } 840 841 func (s *runnerSuite) TestTrustedRepairRootKeys(c *C) { 842 acctKeys := repair.TrustedRepairRootKeys() 843 c.Check(acctKeys, HasLen, 1) 844 c.Check(acctKeys[0].AccountID(), Equals, "canonical") 845 c.Check(acctKeys[0].PublicKeyID(), Equals, "nttW6NfBXI_E-00u38W-KH6eiksfQNXuI7IiumoV49_zkbhM0sYTzSnFlwZC-W4t") 846 } 847 848 func (s *runnerSuite) TestVerify(c *C) { 849 r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) 850 defer r1() 851 r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) 852 defer r2() 853 854 runner := repair.NewRunner() 855 856 a, err := s.repairsSigning.Sign(asserts.RepairType, map[string]interface{}{ 857 "brand-id": "canonical", 858 "repair-id": "2", 859 "summary": "repair two", 860 "timestamp": time.Now().UTC().Format(time.RFC3339), 861 }, []byte("#script"), "") 862 c.Assert(err, IsNil) 863 rpr := a.(*asserts.Repair) 864 865 err = runner.Verify(rpr, []asserts.Assertion{s.repairsAcctKey}) 866 c.Check(err, IsNil) 867 } 868 869 func (s *runnerSuite) signSeqRepairs(c *C, repairs []string) []string { 870 var seq []string 871 for _, rpr := range repairs { 872 decoded, err := asserts.Decode([]byte(rpr)) 873 c.Assert(err, IsNil) 874 signed, err := s.repairsSigning.Sign(asserts.RepairType, decoded.Headers(), decoded.Body(), "") 875 c.Assert(err, IsNil) 876 buf := &bytes.Buffer{} 877 enc := asserts.NewEncoder(buf) 878 enc.Encode(signed) 879 enc.Encode(s.repairsAcctKey) 880 seq = append(seq, buf.String()) 881 } 882 return seq 883 } 884 885 func (s *runnerSuite) loadSequences(c *C) map[string][]*repair.RepairState { 886 data, err := ioutil.ReadFile(dirs.SnapRepairStateFile) 887 c.Assert(err, IsNil) 888 var x struct { 889 Sequences map[string][]*repair.RepairState `json:"sequences"` 890 } 891 err = json.Unmarshal(data, &x) 892 c.Assert(err, IsNil) 893 return x.Sequences 894 } 895 896 func (s *runnerSuite) testNext(c *C, redirectFirst bool) { 897 r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) 898 defer r1() 899 r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) 900 defer r2() 901 902 seqRepairs := s.signSeqRepairs(c, nextRepairs) 903 904 mockServer := makeMockServer(c, &seqRepairs, redirectFirst) 905 defer mockServer.Close() 906 907 runner := repair.NewRunner() 908 runner.BaseURL = mustParseURL(mockServer.URL) 909 runner.LoadState() 910 911 rpr, err := runner.Next("canonical") 912 c.Assert(err, IsNil) 913 c.Check(rpr.RepairID(), Equals, 1) 914 c.Check(osutil.FileExists(filepath.Join(dirs.SnapRepairAssertsDir, "canonical", "1", "r0.repair")), Equals, true) 915 916 rpr, err = runner.Next("canonical") 917 c.Assert(err, IsNil) 918 c.Check(rpr.RepairID(), Equals, 3) 919 c.Check(filepath.Join(dirs.SnapRepairAssertsDir, "canonical", "3", "r2.repair"), testutil.FileEquals, seqRepairs[2]) 920 921 // no more 922 rpr, err = runner.Next("canonical") 923 c.Check(err, Equals, repair.ErrRepairNotFound) 924 925 expectedSeq := []*repair.RepairState{ 926 {Sequence: 1}, 927 {Sequence: 2, Status: repair.SkipStatus}, 928 {Sequence: 3, Revision: 2}, 929 } 930 c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) 931 // on disk 932 seqs := s.loadSequences(c) 933 c.Check(seqs["canonical"], DeepEquals, expectedSeq) 934 935 // start fresh run with new runner 936 // will refetch repair 3 937 signed := s.signSeqRepairs(c, []string{repair3Rev4, repair4}) 938 seqRepairs[2] = signed[0] 939 seqRepairs = append(seqRepairs, signed[1]) 940 941 runner = repair.NewRunner() 942 runner.BaseURL = mustParseURL(mockServer.URL) 943 runner.LoadState() 944 945 rpr, err = runner.Next("canonical") 946 c.Assert(err, IsNil) 947 c.Check(rpr.RepairID(), Equals, 1) 948 949 rpr, err = runner.Next("canonical") 950 c.Assert(err, IsNil) 951 c.Check(rpr.RepairID(), Equals, 3) 952 // refetched new revision! 953 c.Check(rpr.Revision(), Equals, 4) 954 c.Check(rpr.Body(), DeepEquals, []byte("scriptC2\n")) 955 956 // new repair 957 rpr, err = runner.Next("canonical") 958 c.Assert(err, IsNil) 959 c.Check(rpr.RepairID(), Equals, 4) 960 c.Check(rpr.Body(), DeepEquals, []byte("scriptD\n")) 961 962 // no more 963 rpr, err = runner.Next("canonical") 964 c.Check(err, Equals, repair.ErrRepairNotFound) 965 966 c.Check(runner.Sequence("canonical"), DeepEquals, []*repair.RepairState{ 967 {Sequence: 1}, 968 {Sequence: 2, Status: repair.SkipStatus}, 969 {Sequence: 3, Revision: 4}, 970 {Sequence: 4}, 971 }) 972 } 973 974 func (s *runnerSuite) TestNext(c *C) { 975 redirectFirst := false 976 s.testNext(c, redirectFirst) 977 } 978 979 func (s *runnerSuite) TestNextRedirect(c *C) { 980 redirectFirst := true 981 s.testNext(c, redirectFirst) 982 } 983 984 func (s *runnerSuite) TestNextImmediateSkip(c *C) { 985 seqRepairs := []string{`type: repair 986 authority-id: canonical 987 brand-id: canonical 988 repair-id: 1 989 summary: repair one 990 series: 991 - 33 992 timestamp: 2017-07-02T12:00:00Z 993 body-length: 8 994 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 995 996 scriptB 997 998 999 AXNpZw==`} 1000 1001 mockServer := makeMockServer(c, &seqRepairs, false) 1002 defer mockServer.Close() 1003 1004 runner := repair.NewRunner() 1005 runner.BaseURL = mustParseURL(mockServer.URL) 1006 runner.LoadState() 1007 1008 // not applicable => not returned 1009 _, err := runner.Next("canonical") 1010 c.Check(err, Equals, repair.ErrRepairNotFound) 1011 1012 expectedSeq := []*repair.RepairState{ 1013 {Sequence: 1, Status: repair.SkipStatus}, 1014 } 1015 c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) 1016 // on disk 1017 seqs := s.loadSequences(c) 1018 c.Check(seqs["canonical"], DeepEquals, expectedSeq) 1019 } 1020 1021 func (s *runnerSuite) TestNextRefetchSkip(c *C) { 1022 seqRepairs := []string{`type: repair 1023 authority-id: canonical 1024 brand-id: canonical 1025 repair-id: 1 1026 summary: repair one 1027 series: 1028 - 16 1029 timestamp: 2017-07-02T12:00:00Z 1030 body-length: 8 1031 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 1032 1033 scriptB 1034 1035 1036 AXNpZw==`} 1037 1038 r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) 1039 defer r1() 1040 r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) 1041 defer r2() 1042 1043 seqRepairs = s.signSeqRepairs(c, seqRepairs) 1044 1045 mockServer := makeMockServer(c, &seqRepairs, false) 1046 defer mockServer.Close() 1047 1048 runner := repair.NewRunner() 1049 runner.BaseURL = mustParseURL(mockServer.URL) 1050 runner.LoadState() 1051 1052 _, err := runner.Next("canonical") 1053 c.Assert(err, IsNil) 1054 1055 expectedSeq := []*repair.RepairState{ 1056 {Sequence: 1}, 1057 } 1058 c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) 1059 // on disk 1060 seqs := s.loadSequences(c) 1061 c.Check(seqs["canonical"], DeepEquals, expectedSeq) 1062 1063 // new fresh run, repair becomes now unapplicable 1064 seqRepairs[0] = `type: repair 1065 authority-id: canonical 1066 revision: 1 1067 brand-id: canonical 1068 repair-id: 1 1069 summary: repair one rev1 1070 series: 1071 - 16 1072 disabled: true 1073 timestamp: 2017-07-02T12:00:00Z 1074 body-length: 7 1075 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 1076 1077 scriptX 1078 1079 AXNpZw==` 1080 1081 seqRepairs = s.signSeqRepairs(c, seqRepairs) 1082 1083 runner = repair.NewRunner() 1084 runner.BaseURL = mustParseURL(mockServer.URL) 1085 runner.LoadState() 1086 1087 _, err = runner.Next("canonical") 1088 c.Check(err, Equals, repair.ErrRepairNotFound) 1089 1090 expectedSeq = []*repair.RepairState{ 1091 {Sequence: 1, Revision: 1, Status: repair.SkipStatus}, 1092 } 1093 c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) 1094 // on disk 1095 seqs = s.loadSequences(c) 1096 c.Check(seqs["canonical"], DeepEquals, expectedSeq) 1097 } 1098 1099 func (s *runnerSuite) TestNext500(c *C) { 1100 restore := repair.MockPeekRetryStrategy(testRetryStrategy) 1101 defer restore() 1102 1103 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1104 w.WriteHeader(500) 1105 })) 1106 1107 c.Assert(mockServer, NotNil) 1108 defer mockServer.Close() 1109 1110 runner := repair.NewRunner() 1111 runner.BaseURL = mustParseURL(mockServer.URL) 1112 runner.LoadState() 1113 1114 _, err := runner.Next("canonical") 1115 c.Assert(err, ErrorMatches, "cannot peek repair headers, unexpected status 500") 1116 } 1117 1118 func (s *runnerSuite) TestNextNotFound(c *C) { 1119 s.freshState(c) 1120 1121 restore := repair.MockPeekRetryStrategy(testRetryStrategy) 1122 defer restore() 1123 1124 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1125 w.WriteHeader(404) 1126 })) 1127 1128 c.Assert(mockServer, NotNil) 1129 defer mockServer.Close() 1130 1131 runner := repair.NewRunner() 1132 runner.BaseURL = mustParseURL(mockServer.URL) 1133 runner.LoadState() 1134 1135 // sanity 1136 c.Check(dirs.SnapRepairStateFile, testutil.FileEquals, freshStateJSON) 1137 1138 _, err := runner.Next("canonical") 1139 c.Assert(err, Equals, repair.ErrRepairNotFound) 1140 1141 // we saved new time lower bound 1142 t1 := runner.TimeLowerBound() 1143 expected := strings.Replace(freshStateJSON, "2017-08-11T15:49:49Z", t1.Format(time.RFC3339), 1) 1144 c.Check(expected, Not(Equals), freshStateJSON) 1145 c.Check(dirs.SnapRepairStateFile, testutil.FileEquals, expected) 1146 } 1147 1148 func (s *runnerSuite) TestNextSaveStateError(c *C) { 1149 seqRepairs := []string{`type: repair 1150 authority-id: canonical 1151 brand-id: canonical 1152 repair-id: 1 1153 summary: repair one 1154 series: 1155 - 33 1156 timestamp: 2017-07-02T12:00:00Z 1157 body-length: 8 1158 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 1159 1160 scriptB 1161 1162 1163 AXNpZw==`} 1164 1165 mockServer := makeMockServer(c, &seqRepairs, false) 1166 defer mockServer.Close() 1167 1168 runner := repair.NewRunner() 1169 runner.BaseURL = mustParseURL(mockServer.URL) 1170 runner.LoadState() 1171 1172 // break SaveState 1173 restore := makeReadOnly(c, dirs.SnapRepairDir) 1174 defer restore() 1175 1176 _, err := runner.Next("canonical") 1177 c.Check(err, ErrorMatches, `cannot save repair state:.*`) 1178 } 1179 1180 func (s *runnerSuite) TestNextVerifyNoKey(c *C) { 1181 seqRepairs := []string{`type: repair 1182 authority-id: canonical 1183 brand-id: canonical 1184 repair-id: 1 1185 summary: repair one 1186 timestamp: 2017-07-02T12:00:00Z 1187 body-length: 8 1188 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 1189 1190 scriptB 1191 1192 1193 AXNpZw==`} 1194 1195 mockServer := makeMockServer(c, &seqRepairs, false) 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.Check(err, ErrorMatches, `cannot verify repair canonical-1: cannot find public key.*`) 1204 1205 c.Check(runner.Sequence("canonical"), HasLen, 0) 1206 } 1207 1208 func (s *runnerSuite) TestNextVerifySelfSigned(c *C) { 1209 randoKey, _ := assertstest.GenerateKey(752) 1210 1211 randomSigning := assertstest.NewSigningDB("canonical", randoKey) 1212 randoKeyEncoded, err := asserts.EncodePublicKey(randoKey.PublicKey()) 1213 c.Assert(err, IsNil) 1214 acctKey, err := randomSigning.Sign(asserts.AccountKeyType, map[string]interface{}{ 1215 "authority-id": "canonical", 1216 "account-id": "canonical", 1217 "public-key-sha3-384": randoKey.PublicKey().ID(), 1218 "name": "repairs", 1219 "since": time.Now().UTC().Format(time.RFC3339), 1220 }, randoKeyEncoded, "") 1221 c.Assert(err, IsNil) 1222 1223 rpr, err := randomSigning.Sign(asserts.RepairType, map[string]interface{}{ 1224 "brand-id": "canonical", 1225 "repair-id": "1", 1226 "summary": "repair one", 1227 "timestamp": time.Now().UTC().Format(time.RFC3339), 1228 }, []byte("scriptB\n"), "") 1229 c.Assert(err, IsNil) 1230 1231 buf := &bytes.Buffer{} 1232 enc := asserts.NewEncoder(buf) 1233 enc.Encode(rpr) 1234 enc.Encode(acctKey) 1235 seqRepairs := []string{buf.String()} 1236 1237 mockServer := makeMockServer(c, &seqRepairs, false) 1238 defer mockServer.Close() 1239 1240 runner := repair.NewRunner() 1241 runner.BaseURL = mustParseURL(mockServer.URL) 1242 runner.LoadState() 1243 1244 _, err = runner.Next("canonical") 1245 c.Check(err, ErrorMatches, `cannot verify repair canonical-1: circular assertions`) 1246 1247 c.Check(runner.Sequence("canonical"), HasLen, 0) 1248 } 1249 1250 func (s *runnerSuite) TestNextVerifyAllKeysOK(c *C) { 1251 r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) 1252 defer r1() 1253 r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) 1254 defer r2() 1255 1256 decoded, err := asserts.Decode([]byte(nextRepairs[0])) 1257 c.Assert(err, IsNil) 1258 signed, err := s.repairsSigning.Sign(asserts.RepairType, decoded.Headers(), decoded.Body(), "") 1259 c.Assert(err, IsNil) 1260 1261 // stream with all keys (any order) works as well 1262 buf := &bytes.Buffer{} 1263 enc := asserts.NewEncoder(buf) 1264 enc.Encode(signed) 1265 enc.Encode(s.storeSigning.TrustedKey) 1266 enc.Encode(s.repairRootAcctKey) 1267 enc.Encode(s.repairsAcctKey) 1268 seqRepairs := []string{buf.String()} 1269 1270 mockServer := makeMockServer(c, &seqRepairs, false) 1271 defer mockServer.Close() 1272 1273 runner := repair.NewRunner() 1274 runner.BaseURL = mustParseURL(mockServer.URL) 1275 runner.LoadState() 1276 1277 rpr, err := runner.Next("canonical") 1278 c.Assert(err, IsNil) 1279 c.Check(rpr.RepairID(), Equals, 1) 1280 } 1281 1282 func (s *runnerSuite) TestRepairSetStatus(c *C) { 1283 seqRepairs := []string{`type: repair 1284 authority-id: canonical 1285 brand-id: canonical 1286 repair-id: 1 1287 summary: repair one 1288 timestamp: 2017-07-02T12:00:00Z 1289 body-length: 8 1290 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 1291 1292 scriptB 1293 1294 1295 AXNpZw==`} 1296 1297 r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) 1298 defer r1() 1299 r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) 1300 defer r2() 1301 1302 seqRepairs = s.signSeqRepairs(c, seqRepairs) 1303 1304 mockServer := makeMockServer(c, &seqRepairs, false) 1305 defer mockServer.Close() 1306 1307 runner := repair.NewRunner() 1308 runner.BaseURL = mustParseURL(mockServer.URL) 1309 runner.LoadState() 1310 1311 rpr, err := runner.Next("canonical") 1312 c.Assert(err, IsNil) 1313 1314 rpr.SetStatus(repair.DoneStatus) 1315 1316 expectedSeq := []*repair.RepairState{ 1317 {Sequence: 1, Status: repair.DoneStatus}, 1318 } 1319 c.Check(runner.Sequence("canonical"), DeepEquals, expectedSeq) 1320 // on disk 1321 seqs := s.loadSequences(c) 1322 c.Check(seqs["canonical"], DeepEquals, expectedSeq) 1323 } 1324 1325 func (s *runnerSuite) TestRepairBasicRun(c *C) { 1326 seqRepairs := []string{`type: repair 1327 authority-id: canonical 1328 brand-id: canonical 1329 repair-id: 1 1330 summary: repair one 1331 series: 1332 - 16 1333 timestamp: 2017-07-02T12:00:00Z 1334 body-length: 17 1335 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 1336 1337 #!/bin/sh 1338 exit 0 1339 1340 1341 AXNpZw==`} 1342 1343 r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) 1344 defer r1() 1345 r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) 1346 defer r2() 1347 1348 seqRepairs = s.signSeqRepairs(c, seqRepairs) 1349 1350 mockServer := makeMockServer(c, &seqRepairs, false) 1351 defer mockServer.Close() 1352 1353 runner := repair.NewRunner() 1354 runner.BaseURL = mustParseURL(mockServer.URL) 1355 runner.LoadState() 1356 1357 rpr, err := runner.Next("canonical") 1358 c.Assert(err, IsNil) 1359 1360 err = rpr.Run() 1361 c.Assert(err, IsNil) 1362 c.Check(filepath.Join(dirs.SnapRepairRunDir, "canonical", "1", "r0.script"), testutil.FileEquals, "#!/bin/sh\nexit 0\n") 1363 } 1364 1365 func makeMockRepair(script string) string { 1366 return fmt.Sprintf(`type: repair 1367 authority-id: canonical 1368 brand-id: canonical 1369 repair-id: 1 1370 summary: repair one 1371 series: 1372 - 16 1373 timestamp: 2017-07-02T12:00:00Z 1374 body-length: %d 1375 sign-key-sha3-384: KPIl7M4vQ9d4AUjkoU41TGAwtOMLc_bWUCeW8AvdRWD4_xcP60Oo4ABsFNo6BtXj 1376 1377 %s 1378 1379 AXNpZw==`, len(script), script) 1380 } 1381 1382 func verifyRepairStatus(c *C, status repair.RepairStatus) { 1383 c.Check(dirs.SnapRepairStateFile, testutil.FileContains, fmt.Sprintf(`{"device":{"brand":"","model":""},"sequences":{"canonical":[{"sequence":1,"revision":0,"status":%d}`, status)) 1384 } 1385 1386 // tests related to correct execution of script 1387 type runScriptSuite struct { 1388 baseRunnerSuite 1389 1390 seqRepairs []string 1391 1392 mockServer *httptest.Server 1393 runner *repair.Runner 1394 1395 runDir string 1396 1397 restoreErrTrackerReportRepair func() 1398 errReport struct { 1399 repair string 1400 errMsg string 1401 dupSig string 1402 extra map[string]string 1403 } 1404 } 1405 1406 var _ = Suite(&runScriptSuite{}) 1407 1408 func (s *runScriptSuite) SetUpTest(c *C) { 1409 s.baseRunnerSuite.SetUpTest(c) 1410 1411 s.mockServer = makeMockServer(c, &s.seqRepairs, false) 1412 s.AddCleanup(func() { s.mockServer.Close() }) 1413 1414 s.runner = repair.NewRunner() 1415 s.runner.BaseURL = mustParseURL(s.mockServer.URL) 1416 s.runner.LoadState() 1417 1418 s.runDir = filepath.Join(dirs.SnapRepairRunDir, "canonical", "1") 1419 1420 restoreErrTrackerReportRepair := repair.MockErrtrackerReportRepair(s.errtrackerReportRepair) 1421 s.AddCleanup(restoreErrTrackerReportRepair) 1422 } 1423 1424 func (s *runScriptSuite) errtrackerReportRepair(repair, errMsg, dupSig string, extra map[string]string) (string, error) { 1425 s.errReport.repair = repair 1426 s.errReport.errMsg = errMsg 1427 s.errReport.dupSig = dupSig 1428 s.errReport.extra = extra 1429 1430 return "some-oops-id", nil 1431 } 1432 1433 func (s *runScriptSuite) testScriptRun(c *C, mockScript string) *repair.Repair { 1434 r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) 1435 defer r1() 1436 r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) 1437 defer r2() 1438 1439 s.seqRepairs = s.signSeqRepairs(c, s.seqRepairs) 1440 1441 rpr, err := s.runner.Next("canonical") 1442 c.Assert(err, IsNil) 1443 1444 err = rpr.Run() 1445 c.Assert(err, IsNil) 1446 1447 c.Check(filepath.Join(s.runDir, "r0.script"), testutil.FileEquals, mockScript) 1448 1449 return rpr 1450 } 1451 1452 func (s *runScriptSuite) verifyRundir(c *C, names []string) { 1453 dirents, err := ioutil.ReadDir(s.runDir) 1454 c.Assert(err, IsNil) 1455 c.Assert(dirents, HasLen, len(names)) 1456 for i := range dirents { 1457 c.Check(dirents[i].Name(), Matches, names[i]) 1458 } 1459 } 1460 1461 type byMtime []os.FileInfo 1462 1463 func (m byMtime) Len() int { return len(m) } 1464 func (m byMtime) Less(i, j int) bool { return m[i].ModTime().Before(m[j].ModTime()) } 1465 func (m byMtime) Swap(i, j int) { m[i], m[j] = m[j], m[i] } 1466 1467 func (s *runScriptSuite) verifyOutput(c *C, name, expectedOutput string) { 1468 c.Check(filepath.Join(s.runDir, name), testutil.FileEquals, expectedOutput) 1469 // ensure correct permissions 1470 fi, err := os.Stat(filepath.Join(s.runDir, name)) 1471 c.Assert(err, IsNil) 1472 c.Check(fi.Mode(), Equals, os.FileMode(0600)) 1473 } 1474 1475 func (s *runScriptSuite) TestRepairBasicRunHappy(c *C) { 1476 script := `#!/bin/sh 1477 echo "happy output" 1478 echo "done" >&$SNAP_REPAIR_STATUS_FD 1479 exit 0 1480 ` 1481 s.seqRepairs = []string{makeMockRepair(script)} 1482 s.testScriptRun(c, script) 1483 // verify 1484 s.verifyRundir(c, []string{ 1485 `^r0.done$`, 1486 `^r0.script$`, 1487 `^work$`, 1488 }) 1489 s.verifyOutput(c, "r0.done", `repair: canonical-1 1490 revision: 0 1491 summary: repair one 1492 output: 1493 happy output 1494 `) 1495 verifyRepairStatus(c, repair.DoneStatus) 1496 } 1497 1498 func (s *runScriptSuite) TestRepairBasicRunUnhappy(c *C) { 1499 script := `#!/bin/sh 1500 echo "unhappy output" 1501 exit 1 1502 ` 1503 s.seqRepairs = []string{makeMockRepair(script)} 1504 s.testScriptRun(c, script) 1505 // verify 1506 s.verifyRundir(c, []string{ 1507 `^r0.retry$`, 1508 `^r0.script$`, 1509 `^work$`, 1510 }) 1511 s.verifyOutput(c, "r0.retry", `repair: canonical-1 1512 revision: 0 1513 summary: repair one 1514 output: 1515 unhappy output 1516 1517 repair canonical-1 revision 0 failed: exit status 1`) 1518 verifyRepairStatus(c, repair.RetryStatus) 1519 1520 c.Check(s.errReport.repair, Equals, "canonical/1") 1521 c.Check(s.errReport.errMsg, Equals, `repair canonical-1 revision 0 failed: exit status 1`) 1522 c.Check(s.errReport.dupSig, Equals, `canonical/1 1523 repair canonical-1 revision 0 failed: exit status 1 1524 output: 1525 repair: canonical-1 1526 revision: 0 1527 summary: repair one 1528 output: 1529 unhappy output 1530 `) 1531 c.Check(s.errReport.extra, DeepEquals, map[string]string{ 1532 "Revision": "0", 1533 "RepairID": "1", 1534 "BrandID": "canonical", 1535 "Status": "retry", 1536 }) 1537 } 1538 1539 func (s *runScriptSuite) TestRepairBasicSkip(c *C) { 1540 script := `#!/bin/sh 1541 echo "other output" 1542 echo "skip" >&$SNAP_REPAIR_STATUS_FD 1543 exit 0 1544 ` 1545 s.seqRepairs = []string{makeMockRepair(script)} 1546 s.testScriptRun(c, script) 1547 // verify 1548 s.verifyRundir(c, []string{ 1549 `^r0.script$`, 1550 `^r0.skip$`, 1551 `^work$`, 1552 }) 1553 s.verifyOutput(c, "r0.skip", `repair: canonical-1 1554 revision: 0 1555 summary: repair one 1556 output: 1557 other output 1558 `) 1559 verifyRepairStatus(c, repair.SkipStatus) 1560 } 1561 1562 func (s *runScriptSuite) TestRepairBasicRunUnhappyThenHappy(c *C) { 1563 script := `#!/bin/sh 1564 if [ -f zzz-ran-once ]; then 1565 echo "happy now" 1566 echo "done" >&$SNAP_REPAIR_STATUS_FD 1567 exit 0 1568 fi 1569 echo "unhappy output" 1570 touch zzz-ran-once 1571 exit 1 1572 ` 1573 s.seqRepairs = []string{makeMockRepair(script)} 1574 rpr := s.testScriptRun(c, script) 1575 s.verifyRundir(c, []string{ 1576 `^r0.retry$`, 1577 `^r0.script$`, 1578 `^work$`, 1579 }) 1580 s.verifyOutput(c, "r0.retry", `repair: canonical-1 1581 revision: 0 1582 summary: repair one 1583 output: 1584 unhappy output 1585 1586 repair canonical-1 revision 0 failed: exit status 1`) 1587 verifyRepairStatus(c, repair.RetryStatus) 1588 1589 // run again, it will be happy this time 1590 err := rpr.Run() 1591 c.Assert(err, IsNil) 1592 1593 s.verifyRundir(c, []string{ 1594 `^r0.done$`, 1595 `^r0.retry$`, 1596 `^r0.script$`, 1597 `^work$`, 1598 }) 1599 s.verifyOutput(c, "r0.done", `repair: canonical-1 1600 revision: 0 1601 summary: repair one 1602 output: 1603 happy now 1604 `) 1605 verifyRepairStatus(c, repair.DoneStatus) 1606 } 1607 1608 func (s *runScriptSuite) TestRepairHitsTimeout(c *C) { 1609 r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) 1610 defer r1() 1611 r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) 1612 defer r2() 1613 1614 restore := repair.MockDefaultRepairTimeout(100 * time.Millisecond) 1615 defer restore() 1616 1617 script := `#!/bin/sh 1618 echo "output before timeout" 1619 sleep 100 1620 ` 1621 s.seqRepairs = []string{makeMockRepair(script)} 1622 s.seqRepairs = s.signSeqRepairs(c, s.seqRepairs) 1623 1624 rpr, err := s.runner.Next("canonical") 1625 c.Assert(err, IsNil) 1626 1627 err = rpr.Run() 1628 c.Assert(err, IsNil) 1629 1630 s.verifyRundir(c, []string{ 1631 `^r0.retry$`, 1632 `^r0.script$`, 1633 `^work$`, 1634 }) 1635 s.verifyOutput(c, "r0.retry", `repair: canonical-1 1636 revision: 0 1637 summary: repair one 1638 output: 1639 output before timeout 1640 1641 repair canonical-1 revision 0 failed: repair did not finish within 100ms`) 1642 verifyRepairStatus(c, repair.RetryStatus) 1643 } 1644 1645 func (s *runScriptSuite) TestRepairHasCorrectPath(c *C) { 1646 r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) 1647 defer r1() 1648 r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) 1649 defer r2() 1650 1651 script := `#!/bin/sh 1652 echo PATH=$PATH 1653 ls -l ${PATH##*:}/repair 1654 ` 1655 s.seqRepairs = []string{makeMockRepair(script)} 1656 s.seqRepairs = s.signSeqRepairs(c, s.seqRepairs) 1657 1658 rpr, err := s.runner.Next("canonical") 1659 c.Assert(err, IsNil) 1660 1661 err = rpr.Run() 1662 c.Assert(err, IsNil) 1663 1664 c.Check(filepath.Join(s.runDir, "r0.retry"), testutil.FileMatches, fmt.Sprintf(`(?ms).*^PATH=.*:.*/run/snapd/repair/tools.*`)) 1665 c.Check(filepath.Join(s.runDir, "r0.retry"), testutil.FileContains, `/repair -> /usr/lib/snapd/snap-repair`) 1666 1667 // run again and ensure no error happens 1668 err = rpr.Run() 1669 c.Assert(err, IsNil) 1670 1671 } 1672 1673 // shared1620RunnerSuite is embedded by runner16Suite and 1674 // runner20Suite and the tests are run once with a simulated uc16 and 1675 // once with a simulated uc20 environment 1676 type shared1620RunnerSuite struct { 1677 baseRunnerSuite 1678 1679 writeSeedAssert func(c *C, fname string, a asserts.Assertion) 1680 } 1681 1682 func (s *shared1620RunnerSuite) TestTLSTime(c *C) { 1683 s.freshState(c) 1684 runner := repair.NewRunner() 1685 err := runner.LoadState() 1686 c.Assert(err, IsNil) 1687 epoch := time.Unix(0, 0) 1688 r := repair.MockTimeNow(func() time.Time { 1689 return epoch 1690 }) 1691 defer r() 1692 c.Check(runner.TLSTime().Equal(s.seedTime), Equals, true) 1693 } 1694 1695 func (s *shared1620RunnerSuite) TestLoadStateInitState(c *C) { 1696 // sanity 1697 c.Check(osutil.IsDirectory(dirs.SnapRepairDir), Equals, false) 1698 c.Check(osutil.FileExists(dirs.SnapRepairStateFile), Equals, false) 1699 // setup realistic seed/assertions 1700 r := sysdb.InjectTrusted(s.storeSigning.Trusted) 1701 defer r() 1702 s.writeSeedAssert(c, "store.account-key", s.storeSigning.StoreAccountKey("")) 1703 s.writeSeedAssert(c, "brand.account", s.brandAcct) 1704 s.writeSeedAssert(c, "brand.account-key", s.brandAcctKey) 1705 s.writeSeedAssert(c, "model", s.modelAs) 1706 1707 runner := repair.NewRunner() 1708 err := runner.LoadState() 1709 c.Assert(err, IsNil) 1710 c.Check(osutil.FileExists(dirs.SnapRepairStateFile), Equals, true) 1711 1712 brand, model := runner.BrandModel() 1713 c.Check(brand, Equals, "my-brand") 1714 c.Check(model, Equals, "my-model-2") 1715 1716 c.Check(runner.TimeLowerBound().Equal(s.seedTime), Equals, true) 1717 } 1718 1719 type runner16Suite struct { 1720 shared1620RunnerSuite 1721 } 1722 1723 var _ = Suite(&runner16Suite{}) 1724 1725 func (s *runner16Suite) SetUpTest(c *C) { 1726 s.shared1620RunnerSuite.SetUpTest(c) 1727 1728 s.seedAssertsDir = filepath.Join(dirs.SnapSeedDir, "assertions") 1729 1730 // dummy seed yaml 1731 err := os.MkdirAll(s.seedAssertsDir, 0755) 1732 c.Assert(err, IsNil) 1733 seedYamlFn := filepath.Join(dirs.SnapSeedDir, "seed.yaml") 1734 err = ioutil.WriteFile(seedYamlFn, nil, 0644) 1735 c.Assert(err, IsNil) 1736 seedTime, err := time.Parse(time.RFC3339, "2017-08-11T15:49:49Z") 1737 c.Assert(err, IsNil) 1738 err = os.Chtimes(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), seedTime, seedTime) 1739 c.Assert(err, IsNil) 1740 s.seedTime = seedTime 1741 1742 s.t0 = time.Now().UTC().Truncate(time.Minute) 1743 1744 s.writeSeedAssert = s.writeSeedAssert16 1745 } 1746 1747 func (s *runner16Suite) writeSeedAssert16(c *C, fname string, a asserts.Assertion) { 1748 err := ioutil.WriteFile(filepath.Join(s.seedAssertsDir, fname), asserts.Encode(a), 0644) 1749 c.Assert(err, IsNil) 1750 } 1751 1752 func (s *runner16Suite) rmSeedAssert16(c *C, fname string) { 1753 err := os.Remove(filepath.Join(s.seedAssertsDir, fname)) 1754 c.Assert(err, IsNil) 1755 } 1756 1757 func (s *runner16Suite) TestLoadStateInitDeviceInfoFail(c *C) { 1758 // sanity 1759 c.Check(osutil.IsDirectory(dirs.SnapRepairDir), Equals, false) 1760 c.Check(osutil.FileExists(dirs.SnapRepairStateFile), Equals, false) 1761 // setup realistic seed/assertions 1762 r := sysdb.InjectTrusted(s.storeSigning.Trusted) 1763 defer r() 1764 1765 const errPrefix = "cannot set device information: " 1766 tests := []struct { 1767 breakFunc func() 1768 expectedErr string 1769 }{ 1770 {func() { s.rmSeedAssert16(c, "model") }, errPrefix + "no model assertion in seed data"}, 1771 {func() { s.rmSeedAssert16(c, "brand.account") }, errPrefix + "no brand account assertion in seed data"}, 1772 {func() { s.rmSeedAssert16(c, "brand.account-key") }, errPrefix + `cannot find public key.*`}, 1773 {func() { 1774 // broken signature 1775 blob := asserts.Encode(s.brandAcct) 1776 err := ioutil.WriteFile(filepath.Join(s.seedAssertsDir, "brand.account"), blob[:len(blob)-3], 0644) 1777 c.Assert(err, IsNil) 1778 }, errPrefix + "cannot decode signature:.*"}, 1779 {func() { s.writeSeedAssert(c, "model2", s.modelAs) }, errPrefix + "multiple models in seed assertions"}, 1780 } 1781 1782 for _, test := range tests { 1783 s.writeSeedAssert(c, "store.account-key", s.storeSigning.StoreAccountKey("")) 1784 s.writeSeedAssert(c, "brand.account", s.brandAcct) 1785 s.writeSeedAssert(c, "brand.account-key", s.brandAcctKey) 1786 s.writeSeedAssert(c, "model", s.modelAs) 1787 1788 test.breakFunc() 1789 1790 runner := repair.NewRunner() 1791 err := runner.LoadState() 1792 c.Check(err, ErrorMatches, test.expectedErr) 1793 } 1794 } 1795 1796 type runner20Suite struct { 1797 shared1620RunnerSuite 1798 } 1799 1800 var _ = Suite(&runner20Suite{}) 1801 1802 var mockModeenv = []byte(` 1803 mode=run 1804 model=my-brand/my-model-2 1805 `) 1806 1807 func (s *runner20Suite) SetUpTest(c *C) { 1808 s.shared1620RunnerSuite.SetUpTest(c) 1809 1810 s.seedAssertsDir = filepath.Join(dirs.SnapSeedDir, "/systems/20201212/assertions") 1811 err := os.MkdirAll(s.seedAssertsDir, 0755) 1812 c.Assert(err, IsNil) 1813 1814 // write dummy modeenv 1815 err = os.MkdirAll(filepath.Dir(dirs.SnapModeenvFile), 0755) 1816 c.Assert(err, IsNil) 1817 err = ioutil.WriteFile(dirs.SnapModeenvFile, mockModeenv, 0644) 1818 c.Assert(err, IsNil) 1819 // validate that modeenv is actually valid 1820 _, err = boot.ReadModeenv("") 1821 c.Assert(err, IsNil) 1822 1823 seedTime, err := time.Parse(time.RFC3339, "2017-08-11T15:49:49Z") 1824 c.Assert(err, IsNil) 1825 err = os.Chtimes(dirs.SnapModeenvFile, seedTime, seedTime) 1826 c.Assert(err, IsNil) 1827 s.seedTime = seedTime 1828 s.t0 = time.Now().UTC().Truncate(time.Minute) 1829 1830 s.writeSeedAssert = s.writeSeedAssert20 1831 } 1832 1833 func (s *runner20Suite) writeSeedAssert20(c *C, fname string, a asserts.Assertion) { 1834 var fn string 1835 if _, ok := a.(*asserts.Model); ok { 1836 fn = filepath.Join(s.seedAssertsDir, "../model") 1837 } else { 1838 fn = filepath.Join(s.seedAssertsDir, fname) 1839 } 1840 err := ioutil.WriteFile(fn, asserts.Encode(a), 0644) 1841 c.Assert(err, IsNil) 1842 1843 // ensure model assertion file has the correct seed time 1844 if _, ok := a.(*asserts.Model); ok { 1845 err = os.Chtimes(fn, s.seedTime, s.seedTime) 1846 c.Assert(err, IsNil) 1847 } 1848 } 1849 1850 func (s *runner20Suite) TestLoadStateInitDeviceInfoModeenvInvalidContent(c *C) { 1851 runner := repair.NewRunner() 1852 1853 for _, tc := range []struct { 1854 modelStr string 1855 expectedErr string 1856 }{ 1857 { 1858 `invalid-key-value`, 1859 "cannot set device information: No option model in section ", 1860 }, { 1861 `model=`, 1862 `cannot set device information: cannot find brand/model in modeenv model string ""`, 1863 }, { 1864 `model=brand-but-no-model`, 1865 `cannot set device information: cannot find brand/model in modeenv model string "brand-but-no-model"`, 1866 }, 1867 } { 1868 err := ioutil.WriteFile(dirs.SnapModeenvFile, []byte(tc.modelStr), 0644) 1869 c.Assert(err, IsNil) 1870 err = runner.LoadState() 1871 c.Check(err, ErrorMatches, tc.expectedErr) 1872 } 1873 } 1874 1875 func (s *runner20Suite) TestLoadStateInitDeviceInfoModeenvIncorrectPermissions(c *C) { 1876 runner := repair.NewRunner() 1877 1878 err := os.Chmod(dirs.SnapModeenvFile, 0300) 1879 c.Assert(err, IsNil) 1880 s.AddCleanup(func() { 1881 err := os.Chmod(dirs.SnapModeenvFile, 0644) 1882 c.Assert(err, IsNil) 1883 }) 1884 err = runner.LoadState() 1885 c.Check(err, ErrorMatches, "cannot set device information: open /.*/modeenv: permission denied") 1886 }