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