github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/overlord/devicestate/devicestate_cloudinit_test.go (about) 1 package devicestate_test 2 3 import ( 4 "bytes" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "path/filepath" 9 "strings" 10 "time" 11 12 . "gopkg.in/check.v1" 13 14 "github.com/snapcore/snapd/dirs" 15 "github.com/snapcore/snapd/logger" 16 "github.com/snapcore/snapd/overlord/auth" 17 "github.com/snapcore/snapd/overlord/devicestate" 18 "github.com/snapcore/snapd/overlord/devicestate/devicestatetest" 19 "github.com/snapcore/snapd/release" 20 "github.com/snapcore/snapd/sysconfig" 21 "github.com/snapcore/snapd/testutil" 22 ) 23 24 type cloudInitBaseSuite struct { 25 deviceMgrBaseSuite 26 logbuf *bytes.Buffer 27 } 28 29 type cloudInitSuite struct { 30 cloudInitBaseSuite 31 } 32 33 var _ = Suite(&cloudInitSuite{}) 34 35 func (s *cloudInitBaseSuite) SetUpTest(c *C) { 36 s.deviceMgrBaseSuite.SetUpTest(c) 37 38 // undo the cloud-init mocking from deviceMgrBaseSuite, since here we 39 // actually want the default function used to be the real one 40 s.restoreCloudInitStatusRestore() 41 42 r := release.MockOnClassic(false) 43 defer r() 44 45 st := s.o.State() 46 st.Lock() 47 st.Set("seeded", true) 48 st.Unlock() 49 50 logbuf, r := logger.MockLogger() 51 s.logbuf = logbuf 52 s.AddCleanup(r) 53 54 // mock /etc/cloud on writable 55 err := os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "etc", "cloud"), 0755) 56 c.Assert(err, IsNil) 57 } 58 59 type cloudInitUC20Suite struct { 60 cloudInitBaseSuite 61 } 62 63 var _ = Suite(&cloudInitUC20Suite{}) 64 65 func (s *cloudInitUC20Suite) SetUpTest(c *C) { 66 s.cloudInitBaseSuite.SetUpTest(c) 67 68 // make a uc20 style dangerous model assertion for the device 69 // note that actually the devicemgr ensure only cares about having a grade 70 // for uc20, it doesn't use the grade for anything right now, the install 71 // handler code however does care about the grade, so here we just default 72 // to signed 73 s.state.Lock() 74 defer s.state.Unlock() 75 76 s.makeModelAssertionInState(c, "canonical", "pc20-model", map[string]interface{}{ 77 "display-name": "UC20 pc model", 78 "architecture": "amd64", 79 "base": "core20", 80 "grade": "signed", 81 "snaps": []interface{}{ 82 map[string]interface{}{ 83 "name": "pc-kernel", 84 "id": "pckernelidididididididididididid", 85 "type": "kernel", 86 "default-channel": "20", 87 }, 88 map[string]interface{}{ 89 "name": "pc", 90 "id": "pcididididididididididididididid", 91 "type": "gadget", 92 "default-channel": "20", 93 }}, 94 }) 95 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 96 Brand: "canonical", 97 Model: "pc20-model", 98 Serial: "serial", 99 }) 100 101 // create the gadget snap's mount dir 102 gadgetDir := filepath.Join(dirs.SnapMountDir, "pc", "1") 103 c.Assert(os.MkdirAll(gadgetDir, 0755), IsNil) 104 } 105 106 func (s *cloudInitUC20Suite) TestCloudInitUC20CloudGadgetNoDisable(c *C) { 107 // create a cloud.conf file in the gadget snap's mount dir 108 c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapMountDir, "pc", "1", "cloud.conf"), nil, 0644), IsNil) 109 110 // pretend that cloud-init finished running 111 statusCalls := 0 112 r := devicestate.MockCloudInitStatus(func() (sysconfig.CloudInitState, error) { 113 statusCalls++ 114 return sysconfig.CloudInitDone, nil 115 }) 116 defer r() 117 118 restrictCalls := 0 119 r = devicestate.MockRestrictCloudInit(func(state sysconfig.CloudInitState, opts *sysconfig.CloudInitRestrictOptions) (sysconfig.CloudInitRestrictionResult, error) { 120 restrictCalls++ 121 c.Assert(state, Equals, sysconfig.CloudInitDone) 122 c.Assert(opts, DeepEquals, &sysconfig.CloudInitRestrictOptions{ 123 DisableAfterLocalDatasourcesRun: true, 124 }) 125 // in this case, pretend it was a real cloud, so it just got restricted 126 return sysconfig.CloudInitRestrictionResult{ 127 Action: "restrict", 128 DataSource: "GCE", 129 }, nil 130 }) 131 defer r() 132 133 err := devicestate.EnsureCloudInitRestricted(s.mgr) 134 c.Assert(err, IsNil) 135 c.Assert(statusCalls, Equals, 1) 136 c.Assert(restrictCalls, Equals, 1) 137 c.Assert(strings.TrimSpace(s.logbuf.String()), Matches, `.*System initialized, cloud-init reported to be done, set datasource_list to \[ GCE \].*`) 138 } 139 140 func (s *cloudInitUC20Suite) TestCloudInitUC20NoCloudGadgetDisables(c *C) { 141 // pretend that cloud-init never ran 142 statusCalls := 0 143 r := devicestate.MockCloudInitStatus(func() (sysconfig.CloudInitState, error) { 144 statusCalls++ 145 return sysconfig.CloudInitUntriggered, nil 146 }) 147 defer r() 148 149 restrictCalls := 0 150 r = devicestate.MockRestrictCloudInit(func(state sysconfig.CloudInitState, opts *sysconfig.CloudInitRestrictOptions) (sysconfig.CloudInitRestrictionResult, error) { 151 restrictCalls++ 152 c.Assert(state, Equals, sysconfig.CloudInitUntriggered) 153 // no gadget cloud.conf, so we should be asked to disable if it was 154 // NoCloud 155 c.Assert(opts, DeepEquals, &sysconfig.CloudInitRestrictOptions{ 156 DisableAfterLocalDatasourcesRun: true, 157 }) 158 // cloud-init never ran, so no datasource 159 return sysconfig.CloudInitRestrictionResult{ 160 Action: "disable", 161 }, nil 162 }) 163 defer r() 164 165 err := devicestate.EnsureCloudInitRestricted(s.mgr) 166 c.Assert(err, IsNil) 167 c.Assert(statusCalls, Equals, 1) 168 c.Assert(restrictCalls, Equals, 1) 169 170 c.Assert(strings.TrimSpace(s.logbuf.String()), Matches, `.*System initialized, cloud-init reported to be in disabled state, disabled permanently.*`) 171 } 172 173 func (s *cloudInitUC20Suite) TestCloudInitDoneNoCloudDisables(c *C) { 174 // pretend that cloud-init ran, and mock the actual cloud-init command to 175 // use the real sysconfig logic 176 cmd := testutil.MockCommand(c, "cloud-init", ` 177 if [ "$1" = "status" ]; then 178 echo "status: done" 179 else 180 echo "unexpected args $*" 181 exit 1 182 fi`) 183 defer cmd.Restore() 184 185 restrictCalls := 0 186 187 r := devicestate.MockRestrictCloudInit(func(state sysconfig.CloudInitState, opts *sysconfig.CloudInitRestrictOptions) (sysconfig.CloudInitRestrictionResult, error) { 188 restrictCalls++ 189 c.Assert(state, Equals, sysconfig.CloudInitDone) 190 c.Assert(opts, DeepEquals, &sysconfig.CloudInitRestrictOptions{ 191 DisableAfterLocalDatasourcesRun: true, 192 }) 193 // we would have disabled it as per the opts 194 return sysconfig.CloudInitRestrictionResult{ 195 // pretend it was NoCloud 196 DataSource: "NoCloud", 197 Action: "disable", 198 }, nil 199 }) 200 defer r() 201 202 err := devicestate.EnsureCloudInitRestricted(s.mgr) 203 c.Assert(err, IsNil) 204 205 c.Assert(cmd.Calls(), DeepEquals, [][]string{ 206 {"cloud-init", "status"}, 207 }) 208 209 // a message about cloud-init done and being restricted 210 c.Assert(strings.TrimSpace(s.logbuf.String()), Matches, `.*System initialized, cloud-init reported to be done, disabled permanently.*`) 211 212 // and 1 call to restrict 213 c.Assert(restrictCalls, Equals, 1) 214 } 215 216 func (s *cloudInitSuite) SetUpTest(c *C) { 217 s.cloudInitBaseSuite.SetUpTest(c) 218 219 // make a uc16/uc18 style model assertion for the device 220 s.state.Lock() 221 defer s.state.Unlock() 222 223 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 224 "architecture": "amd64", 225 "kernel": "pc-kernel", 226 "gadget": "pc", 227 "base": "core18", 228 }) 229 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 230 Brand: "canonical", 231 Model: "pc-model", 232 Serial: "serial", 233 }) 234 } 235 236 func (s *cloudInitSuite) TestClassicCloudInitDoesNothing(c *C) { 237 r := release.MockOnClassic(true) 238 defer r() 239 240 r = devicestate.MockCloudInitStatus(func() (sysconfig.CloudInitState, error) { 241 c.Error("EnsureCloudInitRestricted should not have checked cloud-init status when on classic") 242 return 0, fmt.Errorf("broken") 243 }) 244 defer r() 245 246 r = devicestate.MockRestrictCloudInit(func(sysconfig.CloudInitState, *sysconfig.CloudInitRestrictOptions) (sysconfig.CloudInitRestrictionResult, error) { 247 c.Error("EnsureCloudInitRestricted should not have restricted cloud-init when on classic") 248 return sysconfig.CloudInitRestrictionResult{}, fmt.Errorf("broken") 249 }) 250 defer r() 251 252 err := devicestate.EnsureCloudInitRestricted(s.mgr) 253 c.Assert(err, IsNil) 254 } 255 256 func (s *cloudInitSuite) TestCloudInitEnsureBeforeSeededDoesNothing(c *C) { 257 st := s.o.State() 258 st.Lock() 259 st.Set("seeded", false) 260 st.Unlock() 261 262 r := devicestate.MockCloudInitStatus(func() (sysconfig.CloudInitState, error) { 263 c.Error("EnsureCloudInitRestricted should not have checked cloud-init status when not seeded") 264 return 0, fmt.Errorf("broken") 265 }) 266 defer r() 267 268 r = devicestate.MockRestrictCloudInit(func(sysconfig.CloudInitState, *sysconfig.CloudInitRestrictOptions) (sysconfig.CloudInitRestrictionResult, error) { 269 c.Error("EnsureCloudInitRestricted should not have restricted cloud-init when not seeded") 270 return sysconfig.CloudInitRestrictionResult{}, fmt.Errorf("broken") 271 }) 272 defer r() 273 274 err := devicestate.EnsureCloudInitRestricted(s.mgr) 275 c.Assert(err, IsNil) 276 } 277 278 func (s *cloudInitSuite) TestCloudInitAlreadyEnsuredRestrictedDoesNothing(c *C) { 279 n := 0 280 281 // mock that it was restricted so that we set the internal bool to say it 282 // already ran 283 r := devicestate.MockCloudInitStatus(func() (sysconfig.CloudInitState, error) { 284 n++ 285 switch n { 286 case 1: 287 return sysconfig.CloudInitRestrictedBySnapd, nil 288 default: 289 c.Error("EnsureCloudInitRestricted should not have checked cloud-init status again") 290 return sysconfig.CloudInitRestrictedBySnapd, fmt.Errorf("test broken") 291 } 292 }) 293 defer r() 294 295 // run it once to set the internal bool 296 err := devicestate.EnsureCloudInitRestricted(s.mgr) 297 c.Assert(err, IsNil) 298 299 c.Assert(n, Equals, 1) 300 301 // it should run again without checking anything 302 err = devicestate.EnsureCloudInitRestricted(s.mgr) 303 c.Assert(err, IsNil) 304 305 c.Assert(n, Equals, 1) 306 } 307 308 func (s *cloudInitSuite) TestCloudInitDeviceManagerEnsureRestrictsCloudInit(c *C) { 309 n := 0 310 311 // mock that it was restricted so that we set the internal bool to say it 312 // already ran 313 r := devicestate.MockCloudInitStatus(func() (sysconfig.CloudInitState, error) { 314 n++ 315 switch n { 316 case 1: 317 return sysconfig.CloudInitRestrictedBySnapd, nil 318 default: 319 c.Error("EnsureCloudInitRestricted should not have checked cloud-init status again") 320 return sysconfig.CloudInitRestrictedBySnapd, fmt.Errorf("test broken") 321 } 322 }) 323 defer r() 324 325 // run it once to set the internal bool 326 err := s.mgr.Ensure() 327 c.Assert(err, IsNil) 328 c.Assert(n, Equals, 1) 329 330 // running again is still okay and won't call CloudInitStatus again 331 err = s.mgr.Ensure() 332 c.Assert(err, IsNil) 333 c.Assert(n, Equals, 1) 334 } 335 336 func (s *cloudInitSuite) TestCloudInitAlreadyRestrictedDoesNothing(c *C) { 337 statusCalls := 0 338 r := devicestate.MockCloudInitStatus(func() (sysconfig.CloudInitState, error) { 339 statusCalls++ 340 return sysconfig.CloudInitRestrictedBySnapd, nil 341 }) 342 defer r() 343 344 r = devicestate.MockRestrictCloudInit(func(sysconfig.CloudInitState, *sysconfig.CloudInitRestrictOptions) (sysconfig.CloudInitRestrictionResult, error) { 345 c.Error("EnsureCloudInitRestricted should not have restricted cloud-init when already restricted") 346 return sysconfig.CloudInitRestrictionResult{}, fmt.Errorf("broken") 347 }) 348 defer r() 349 350 err := devicestate.EnsureCloudInitRestricted(s.mgr) 351 c.Assert(err, IsNil) 352 c.Assert(statusCalls, Equals, 1) 353 } 354 355 func (s *cloudInitSuite) TestCloudInitAlreadyRestrictedFileDoesNothing(c *C) { 356 // write a cloud-init restriction file 357 disableFile := filepath.Join(dirs.GlobalRootDir, "/etc/cloud/cloud.cfg.d/zzzz_snapd.cfg") 358 err := os.MkdirAll(filepath.Dir(disableFile), 0755) 359 c.Assert(err, IsNil) 360 err = ioutil.WriteFile(disableFile, nil, 0644) 361 c.Assert(err, IsNil) 362 363 // mock cloud-init command, but make it always fail, it shouldn't be called 364 // as cloud-init.disabled should tell sysconfig to never consult cloud-init 365 // directly 366 cmd := testutil.MockCommand(c, "cloud-init", ` 367 echo "unexpected call to cloud-init with args $*" 368 exit 1`) 369 defer cmd.Restore() 370 371 r := devicestate.MockRestrictCloudInit(func(sysconfig.CloudInitState, *sysconfig.CloudInitRestrictOptions) (sysconfig.CloudInitRestrictionResult, error) { 372 c.Error("EnsureCloudInitRestricted should not have restricted cloud-init when already disabled") 373 return sysconfig.CloudInitRestrictionResult{}, fmt.Errorf("broken") 374 }) 375 defer r() 376 377 err = devicestate.EnsureCloudInitRestricted(s.mgr) 378 c.Assert(err, IsNil) 379 380 c.Assert(s.logbuf.String(), Equals, "") 381 382 c.Assert(cmd.Calls(), HasLen, 0) 383 } 384 385 func (s *cloudInitSuite) TestCloudInitAlreadyDisabledDoesNothing(c *C) { 386 // the absence of a zzzz_snapd.cfg file will indicate that it has not been 387 // restricted yet and thus it should then check to see if it was manually 388 // disabled 389 390 // write a cloud-init disabled file 391 disableFile := filepath.Join(dirs.GlobalRootDir, "/etc/cloud/cloud-init.disabled") 392 err := os.MkdirAll(filepath.Dir(disableFile), 0755) 393 c.Assert(err, IsNil) 394 err = ioutil.WriteFile(disableFile, nil, 0644) 395 c.Assert(err, IsNil) 396 397 // mock cloud-init command, but make it always fail, it shouldn't be called 398 // as cloud-init.disabled should tell sysconfig to never consult cloud-init 399 // directly 400 cmd := testutil.MockCommand(c, "cloud-init", ` 401 echo "unexpected call to cloud-init with args $*" 402 exit 1`) 403 defer cmd.Restore() 404 405 r := devicestate.MockRestrictCloudInit(func(sysconfig.CloudInitState, *sysconfig.CloudInitRestrictOptions) (sysconfig.CloudInitRestrictionResult, error) { 406 c.Error("EnsureCloudInitRestricted should not have restricted cloud-init when already disabled") 407 return sysconfig.CloudInitRestrictionResult{}, fmt.Errorf("broken") 408 }) 409 defer r() 410 411 err = devicestate.EnsureCloudInitRestricted(s.mgr) 412 c.Assert(err, IsNil) 413 414 c.Assert(s.logbuf.String(), Equals, "") 415 416 c.Assert(cmd.Calls(), HasLen, 0) 417 } 418 419 func (s *cloudInitSuite) TestCloudInitUntriggeredDisables(c *C) { 420 // the absence of a zzzz_snapd.cfg file will indicate that it has not been 421 // restricted yet and thus it should then check to see if it was manually 422 // disabled 423 424 // the absence of a cloud-init.disabled file indicates that cloud-init is 425 // "untriggered", i.e. not active/running but could still be triggered 426 427 cmd := testutil.MockCommand(c, "cloud-init", ` 428 if [ "$1" = "status" ]; then 429 echo "status: disabled" 430 else 431 echo "unexpected args $*" 432 exit 1 433 fi`) 434 defer cmd.Restore() 435 436 restrictCalls := 0 437 438 r := devicestate.MockRestrictCloudInit(func(state sysconfig.CloudInitState, opts *sysconfig.CloudInitRestrictOptions) (sysconfig.CloudInitRestrictionResult, error) { 439 restrictCalls++ 440 c.Assert(state, Equals, sysconfig.CloudInitUntriggered) 441 c.Assert(opts, DeepEquals, &sysconfig.CloudInitRestrictOptions{ 442 ForceDisable: false, 443 }) 444 // we would have disabled it 445 return sysconfig.CloudInitRestrictionResult{Action: "disable"}, nil 446 }) 447 defer r() 448 449 err := devicestate.EnsureCloudInitRestricted(s.mgr) 450 c.Assert(err, IsNil) 451 452 c.Assert(cmd.Calls(), DeepEquals, [][]string{ 453 {"cloud-init", "status"}, 454 }) 455 456 // a message about cloud-init done and being restricted 457 c.Assert(strings.TrimSpace(s.logbuf.String()), Matches, `.*System initialized, cloud-init reported to be in disabled state, disabled permanently.*`) 458 459 c.Assert(restrictCalls, Equals, 1) 460 } 461 462 func (s *cloudInitSuite) TestCloudInitDoneRestricts(c *C) { 463 // the absence of a zzzz_snapd.cfg file will indicate that it has not been 464 // restricted yet and thus it should then check to see if it was manually 465 // disabled 466 467 // the absence of a cloud-init.disabled file indicates that cloud-init is 468 // "untriggered", i.e. not active/running but could still be triggered 469 470 cmd := testutil.MockCommand(c, "cloud-init", ` 471 if [ "$1" = "status" ]; then 472 echo "status: done" 473 else 474 echo "unexpected args $*" 475 exit 1 476 fi`) 477 defer cmd.Restore() 478 479 restrictCalls := 0 480 481 r := devicestate.MockRestrictCloudInit(func(state sysconfig.CloudInitState, opts *sysconfig.CloudInitRestrictOptions) (sysconfig.CloudInitRestrictionResult, error) { 482 restrictCalls++ 483 c.Assert(state, Equals, sysconfig.CloudInitDone) 484 c.Assert(opts, DeepEquals, &sysconfig.CloudInitRestrictOptions{ 485 ForceDisable: false, 486 }) 487 // we would have restricted it since it ran 488 return sysconfig.CloudInitRestrictionResult{ 489 // pretend it was NoCloud 490 DataSource: "NoCloud", 491 Action: "restrict", 492 }, nil 493 }) 494 defer r() 495 496 err := devicestate.EnsureCloudInitRestricted(s.mgr) 497 c.Assert(err, IsNil) 498 499 c.Assert(cmd.Calls(), DeepEquals, [][]string{ 500 {"cloud-init", "status"}, 501 }) 502 503 // a message about cloud-init done and being restricted 504 c.Assert(strings.TrimSpace(s.logbuf.String()), Matches, `.*System initialized, cloud-init reported to be done, set datasource_list to \[ NoCloud \] and disabled auto-import by filesystem label.*`) 505 506 // and 1 call to restrict 507 c.Assert(restrictCalls, Equals, 1) 508 } 509 510 func (s *cloudInitSuite) TestCloudInitDoneProperCloudRestricts(c *C) { 511 // the absence of a zzzz_snapd.cfg file will indicate that it has not been 512 // restricted yet and thus it should then check to see if it was manually 513 // disabled 514 515 // the absence of a cloud-init.disabled file indicates that cloud-init is 516 // "untriggered", i.e. not active/running but could still be triggered 517 518 cmd := testutil.MockCommand(c, "cloud-init", ` 519 if [ "$1" = "status" ]; then 520 echo "status: done" 521 else 522 echo "unexpected args $*" 523 exit 1 524 fi`) 525 defer cmd.Restore() 526 527 restrictCalls := 0 528 529 r := devicestate.MockRestrictCloudInit(func(state sysconfig.CloudInitState, opts *sysconfig.CloudInitRestrictOptions) (sysconfig.CloudInitRestrictionResult, error) { 530 restrictCalls++ 531 c.Assert(state, Equals, sysconfig.CloudInitDone) 532 c.Assert(opts, DeepEquals, &sysconfig.CloudInitRestrictOptions{ 533 ForceDisable: false, 534 }) 535 // we would have restricted it since it ran 536 return sysconfig.CloudInitRestrictionResult{ 537 // pretend it was GCE 538 DataSource: "GCE", 539 Action: "restrict", 540 }, nil 541 }) 542 defer r() 543 544 err := devicestate.EnsureCloudInitRestricted(s.mgr) 545 c.Assert(err, IsNil) 546 547 c.Assert(cmd.Calls(), DeepEquals, [][]string{ 548 {"cloud-init", "status"}, 549 }) 550 551 // a message about cloud-init done and being restricted 552 c.Assert(strings.TrimSpace(s.logbuf.String()), Matches, `.*System initialized, cloud-init reported to be done, set datasource_list to \[ GCE \].*`) 553 554 // only called restrict once 555 c.Assert(restrictCalls, Equals, 1) 556 } 557 558 func (s *cloudInitSuite) TestCloudInitRunningEnsuresUntilNotRunning(c *C) { 559 // the absence of a zzzz_snapd.cfg file will indicate that it has not been 560 // restricted yet and thus it should then check to see if it was manually 561 // disabled 562 563 // the absence of a cloud-init.disabled file indicates that cloud-init is 564 // "untriggered", i.e. not active/running but could still be triggered 565 566 // we use a file to make the mocked cloud-init act differently depending on 567 // how many times it is called 568 // this is because we want to test settle()/EnsureBefore() automatically 569 // re-triggering the EnsureCloudInitRestricted() w/o changing the script 570 // mid-way through the test while settle() is running 571 cloudInitScriptStateFile := filepath.Join(c.MkDir(), "cloud-init-state") 572 573 cmd := testutil.MockCommand(c, "cloud-init", fmt.Sprintf(` 574 # the first time the script is called the file shouldn't exist, so return 575 # running 576 # next time when the file exists, return done 577 if [ -f %[1]s ]; then 578 status="done" 579 else 580 status="running" 581 touch %[1]s 582 fi 583 if [ "$1" = "status" ]; then 584 echo "status: $status" 585 else 586 echo "unexpected args $*" 587 exit 1 588 fi`, cloudInitScriptStateFile)) 589 defer cmd.Restore() 590 591 restrictCalls := 0 592 593 r := devicestate.MockRestrictCloudInit(func(state sysconfig.CloudInitState, opts *sysconfig.CloudInitRestrictOptions) (sysconfig.CloudInitRestrictionResult, error) { 594 restrictCalls++ 595 c.Assert(state, Equals, sysconfig.CloudInitDone) 596 c.Assert(opts, DeepEquals, &sysconfig.CloudInitRestrictOptions{ 597 ForceDisable: false, 598 }) 599 // we would have restricted it 600 return sysconfig.CloudInitRestrictionResult{ 601 // pretend it was NoCloud 602 DataSource: "NoCloud", 603 Action: "restrict", 604 }, nil 605 }) 606 defer r() 607 608 err := devicestate.EnsureCloudInitRestricted(s.mgr) 609 c.Assert(err, IsNil) 610 611 // no log messages while we wait for the transition 612 c.Assert(s.logbuf.String(), Equals, "") 613 614 // should not have called to restrict 615 c.Assert(restrictCalls, Equals, 0) 616 617 // only one call to cloud-init status 618 c.Assert(cmd.Calls(), DeepEquals, [][]string{ 619 {"cloud-init", "status"}, 620 }) 621 622 // we should have had a call to EnsureBefore, so if we now settle, we will 623 // see an additional call to cloud-init status, which now returns done and 624 // progresses 625 s.settle(c) 626 627 c.Assert(cmd.Calls(), DeepEquals, [][]string{ 628 {"cloud-init", "status"}, 629 {"cloud-init", "status"}, 630 }) 631 632 // now restrict should have been called 633 c.Assert(restrictCalls, Equals, 1) 634 635 // now a message that it was disabled 636 c.Assert(strings.TrimSpace(s.logbuf.String()), Matches, `.*System initialized, cloud-init reported to be done, set datasource_list to \[ NoCloud \] and disabled auto-import by filesystem label.*`) 637 } 638 639 func (s *cloudInitSuite) TestCloudInitSteadyErrorDisables(c *C) { 640 // the absence of a zzzz_snapd.cfg file will indicate that it has not been 641 // restricted yet and thus it should then check to see if it was manually 642 // disabled 643 644 // the absence of a cloud-init.disabled file indicates that cloud-init is 645 // "untriggered", i.e. not active/running but could still be triggered 646 647 cmd := testutil.MockCommand(c, "cloud-init", ` 648 if [ "$1" = "status" ]; then 649 echo "status: error" 650 else 651 echo "unexpected args $*" 652 exit 1 653 fi`) 654 defer cmd.Restore() 655 656 restrictCalls := 0 657 658 r := devicestate.MockRestrictCloudInit(func(state sysconfig.CloudInitState, opts *sysconfig.CloudInitRestrictOptions) (sysconfig.CloudInitRestrictionResult, error) { 659 restrictCalls++ 660 c.Assert(state, Equals, sysconfig.CloudInitErrored) 661 c.Assert(opts, DeepEquals, &sysconfig.CloudInitRestrictOptions{ 662 ForceDisable: true, 663 }) 664 // we would have disabled it 665 return sysconfig.CloudInitRestrictionResult{ 666 Action: "disable", 667 }, nil 668 }) 669 defer r() 670 671 timeCalls := 0 672 testStart := time.Now() 673 674 r = devicestate.MockTimeNow(func() time.Time { 675 // we will only call time.Now() three times, first to initialize/set the 676 // that we saw cloud-init in error, and another immediately after to 677 // check if the 3 minute timeout has elapsed, and then finally after the 678 // ensure() call happened 3 minutes later 679 timeCalls++ 680 switch timeCalls { 681 case 1, 2: 682 // we have 2 calls that happen at first, the first one initializes 683 // the time we checked it at, and for code simplicity, another one 684 // right after to check if the time elapsed 685 // both of these should have the same time for the first call to 686 // EnsureCloudInitRestricted 687 return testStart 688 case 3: 689 return testStart.Add(3*time.Minute + 1*time.Second) 690 default: 691 c.Errorf("unexpected additional call (number %d) to time.Now()", timeCalls) 692 return time.Time{} 693 } 694 }) 695 defer r() 696 697 err := devicestate.EnsureCloudInitRestricted(s.mgr) 698 c.Assert(err, IsNil) 699 700 // should not have called restrict 701 c.Assert(restrictCalls, Equals, 0) 702 703 // only one call to cloud-init status 704 c.Assert(cmd.Calls(), DeepEquals, [][]string{ 705 {"cloud-init", "status"}, 706 }) 707 708 // a message about error state for the operator to try to fix 709 c.Assert(strings.TrimSpace(s.logbuf.String()), Matches, `.*System initialized, cloud-init reported to be in error state, will disable in 3 minutes.*`) 710 s.logbuf.Reset() 711 712 // make sure the time accounting is correct 713 c.Assert(timeCalls, Equals, 2) 714 715 // we should have had a call to EnsureBefore, so if we now settle, we will 716 // see an additional call to cloud-init status, which continues to return 717 // error and then disables cloud-init 718 s.settle(c) 719 720 // make sure the time accounting is correct after the ensure - one more 721 // check which was simulated to be 3 minutes later 722 c.Assert(timeCalls, Equals, 3) 723 724 c.Assert(cmd.Calls(), DeepEquals, [][]string{ 725 {"cloud-init", "status"}, 726 {"cloud-init", "status"}, 727 }) 728 729 // now restrict should have been called 730 c.Assert(restrictCalls, Equals, 1) 731 732 // and a new message about being disabled permanently 733 c.Assert(strings.TrimSpace(s.logbuf.String()), Matches, `.*System initialized, cloud-init reported to be in error state after 3 minutes, disabled permanently.*`) 734 } 735 736 func (s *cloudInitSuite) TestCloudInitSteadyErrorDisablesFasterEnsure(c *C) { 737 // the absence of a zzzz_snapd.cfg file will indicate that it has not been 738 // restricted yet and thus it should then check to see if it was manually 739 // disabled 740 741 // the absence of a cloud-init.disabled file indicates that cloud-init is 742 // "untriggered", i.e. not active/running but could still be triggered 743 744 cmd := testutil.MockCommand(c, "cloud-init", ` 745 if [ "$1" = "status" ]; then 746 echo "status: error" 747 else 748 echo "unexpected args $*" 749 exit 1 750 fi`) 751 defer cmd.Restore() 752 753 restrictCalls := 0 754 755 r := devicestate.MockRestrictCloudInit(func(state sysconfig.CloudInitState, opts *sysconfig.CloudInitRestrictOptions) (sysconfig.CloudInitRestrictionResult, error) { 756 restrictCalls++ 757 c.Assert(state, Equals, sysconfig.CloudInitErrored) 758 c.Assert(opts, DeepEquals, &sysconfig.CloudInitRestrictOptions{ 759 ForceDisable: true, 760 }) 761 // we would have disabled it 762 return sysconfig.CloudInitRestrictionResult{ 763 Action: "disable", 764 }, nil 765 }) 766 defer r() 767 768 timeCalls := 0 769 testStart := time.Now() 770 771 r = devicestate.MockTimeNow(func() time.Time { 772 // we will only call time.Now() three times, first to initialize/set the 773 // that we saw cloud-init in error, and another immediately after to 774 // check if the 3 minute timeout has elapsed, and then a few odd times 775 // before hitting the timeout to ensure we don't print the log message 776 // unnecessarily and that the timeout logic works 777 timeCalls++ 778 switch timeCalls { 779 case 1, 2: 780 // we have 2 calls that happen at first, the first one initializes 781 // the time we checked it at, and for code simplicity, another one 782 // right after to check if the time elapsed 783 // both of these should have the same time for the first call to 784 // EnsureCloudInitRestricted 785 return testStart 786 case 3: 787 // only 1 minute elapsed 788 return testStart.Add(1 * time.Minute) 789 case 4: 790 // only 1 minute elapsed 791 return testStart.Add(1*time.Minute + 30*time.Second) 792 case 5: 793 // now we hit the timeout 794 return testStart.Add(3*time.Minute + 1*time.Second) 795 default: 796 c.Errorf("unexpected additional call (number %d) to time.Now()", timeCalls) 797 return time.Time{} 798 } 799 }) 800 defer r() 801 802 err := devicestate.EnsureCloudInitRestricted(s.mgr) 803 c.Assert(err, IsNil) 804 805 // should not have called restrict 806 c.Assert(restrictCalls, Equals, 0) 807 808 // only one call to cloud-init status 809 c.Assert(cmd.Calls(), DeepEquals, [][]string{ 810 {"cloud-init", "status"}, 811 }) 812 813 // a message about error state for the operator to try to fix 814 c.Assert(strings.TrimSpace(s.logbuf.String()), Matches, `.*System initialized, cloud-init reported to be in error state, will disable in 3 minutes.*`) 815 s.logbuf.Reset() 816 817 // make sure the time accounting is correct 818 c.Assert(timeCalls, Equals, 2) 819 820 // we should have had a call to EnsureBefore, so if we now settle, we will 821 // see an additional call to cloud-init status, which continues to return 822 // error and then disables cloud-init 823 s.settle(c) 824 825 // make sure the time accounting is correct after the ensure - one more 826 // check which was simulated to be 3 minutes later 827 c.Assert(timeCalls, Equals, 5) 828 829 c.Assert(cmd.Calls(), DeepEquals, [][]string{ 830 {"cloud-init", "status"}, 831 {"cloud-init", "status"}, 832 {"cloud-init", "status"}, 833 {"cloud-init", "status"}, 834 }) 835 836 // now restrict should have been called 837 c.Assert(restrictCalls, Equals, 1) 838 839 // and a new message about being disabled permanently 840 c.Assert(strings.TrimSpace(s.logbuf.String()), Matches, `.*System initialized, cloud-init reported to be in error state after 3 minutes, disabled permanently.*`) 841 } 842 843 func (s *cloudInitSuite) TestCloudInitTakingTooLongDisables(c *C) { 844 // the absence of a zzzz_snapd.cfg file will indicate that it has not been 845 // restricted yet and thus it should then check to see if it was manually 846 // disabled 847 848 // the absence of a cloud-init.disabled file indicates that cloud-init is 849 // "untriggered", i.e. not active/running but could still be triggered 850 851 cmd := testutil.MockCommand(c, "cloud-init", ` 852 if [ "$1" = "status" ]; then 853 echo "status: running" 854 else 855 echo "unexpected args $*" 856 exit 1 857 fi`) 858 defer cmd.Restore() 859 860 restrictCalls := 0 861 862 r := devicestate.MockRestrictCloudInit(func(state sysconfig.CloudInitState, opts *sysconfig.CloudInitRestrictOptions) (sysconfig.CloudInitRestrictionResult, error) { 863 restrictCalls++ 864 c.Assert(state, Equals, sysconfig.CloudInitEnabled) 865 c.Assert(opts, DeepEquals, &sysconfig.CloudInitRestrictOptions{ 866 ForceDisable: true, 867 }) 868 // we would have disabled it 869 return sysconfig.CloudInitRestrictionResult{ 870 Action: "disable", 871 }, nil 872 }) 873 defer r() 874 875 timeCalls := 0 876 testStart := time.Now() 877 878 r = devicestate.MockTimeNow(func() time.Time { 879 timeCalls++ 880 switch { 881 case timeCalls == 1 || timeCalls == 2: 882 // we have 2 calls that happen at first, the first one initializes 883 // the time we checked it at, and for code simplicity, another one 884 // right after to check if the time elapsed 885 // both of these should have the same time for the first call to 886 // EnsureCloudInitRestricted 887 return testStart 888 case timeCalls > 2 && timeCalls <= 31: 889 // 31 here because we should do 30 checks plus one initially 890 return testStart.Add(time.Duration(timeCalls*10) * time.Second) 891 default: 892 c.Errorf("unexpected additional call (number %d) to time.Now()", timeCalls) 893 return time.Time{} 894 } 895 }) 896 defer r() 897 898 err := devicestate.EnsureCloudInitRestricted(s.mgr) 899 c.Assert(err, IsNil) 900 901 // should not have called to disable 902 c.Assert(restrictCalls, Equals, 0) 903 904 // only one call to cloud-init status 905 c.Assert(cmd.Calls(), DeepEquals, [][]string{ 906 {"cloud-init", "status"}, 907 }) 908 909 // make sure our time accounting is still correct 910 c.Assert(timeCalls, Equals, 2) 911 912 // no messages while it waits until the timeout 913 c.Assert(s.logbuf.String(), Equals, ``) 914 915 // we should have had a call to EnsureBefore, so if we now settle, we will 916 // see additional calls to cloud-init status, which continues to always 917 // return an error and so we eventually give up and disable it anyways 918 s.settle(c) 919 920 // make sure our time accounting is still correct 921 c.Assert(timeCalls, Equals, 31) 922 923 // should have called cloud-init status 30 times 924 calls := make([][]string, 30) 925 for i := 0; i < 30; i++ { 926 calls[i] = []string{"cloud-init", "status"} 927 } 928 929 c.Assert(cmd.Calls(), DeepEquals, calls) 930 931 // now disable should have been called 932 c.Assert(restrictCalls, Equals, 1) 933 934 // now a message after we timeout waiting for the transition 935 c.Assert(strings.TrimSpace(s.logbuf.String()), Matches, `.*System initialized, cloud-init failed to transition to done or error state after 5 minutes, disabled permanently.*`) 936 } 937 938 func (s *cloudInitSuite) TestCloudInitTakingTooLongDisablesFasterEnsures(c *C) { 939 // same test as TestCloudInitTakingTooLongDisables, but with a faster 940 // re-ensure cycle to ensure that if we get scheduled to run Ensure() sooner 941 // than expected everything still works 942 943 // the absence of a zzzz_snapd.cfg file will indicate that it has not been 944 // restricted yet and thus it should then check to see if it was manually 945 // disabled 946 947 // the absence of a cloud-init.disabled file indicates that cloud-init is 948 // "untriggered", i.e. not active/running but could still be triggered 949 950 cmd := testutil.MockCommand(c, "cloud-init", ` 951 if [ "$1" = "status" ]; then 952 echo "status: running" 953 else 954 echo "unexpected args $*" 955 exit 1 956 fi`) 957 defer cmd.Restore() 958 959 restrictCalls := 0 960 961 r := devicestate.MockRestrictCloudInit(func(state sysconfig.CloudInitState, opts *sysconfig.CloudInitRestrictOptions) (sysconfig.CloudInitRestrictionResult, error) { 962 restrictCalls++ 963 c.Assert(state, Equals, sysconfig.CloudInitEnabled) 964 c.Assert(opts, DeepEquals, &sysconfig.CloudInitRestrictOptions{ 965 ForceDisable: true, 966 }) 967 // we would have disabled it 968 return sysconfig.CloudInitRestrictionResult{ 969 Action: "disable", 970 }, nil 971 }) 972 defer r() 973 974 timeCalls := 0 975 testStart := time.Now() 976 977 r = devicestate.MockTimeNow(func() time.Time { 978 timeCalls++ 979 switch { 980 case timeCalls == 1 || timeCalls == 2: 981 // we have 2 calls that happen at first, the first one initializes 982 // the time we checked it at, and for code simplicity, another one 983 // right after to check if the time elapsed 984 // both of these should have the same time for the first call to 985 // EnsureCloudInitRestricted 986 return testStart 987 case timeCalls > 2 && timeCalls <= 61: 988 // 31 here because we should do 60 checks plus one initially 989 return testStart.Add(time.Duration(timeCalls*5) * time.Second) 990 default: 991 c.Errorf("unexpected additional call (number %d) to time.Now()", timeCalls) 992 return time.Time{} 993 } 994 }) 995 defer r() 996 997 err := devicestate.EnsureCloudInitRestricted(s.mgr) 998 c.Assert(err, IsNil) 999 1000 // should not have called to disable 1001 c.Assert(restrictCalls, Equals, 0) 1002 1003 // only one call to cloud-init status 1004 c.Assert(cmd.Calls(), DeepEquals, [][]string{ 1005 {"cloud-init", "status"}, 1006 }) 1007 1008 // make sure our time accounting is still correct 1009 c.Assert(timeCalls, Equals, 2) 1010 1011 // no messages while it waits until the timeout 1012 c.Assert(s.logbuf.String(), Equals, ``) 1013 1014 // we should have had a call to EnsureBefore, so if we now settle, we will 1015 // see additional calls to cloud-init status, which continues to always 1016 // return an error and so we eventually give up and disable it anyways 1017 s.settle(c) 1018 1019 // make sure our time accounting is still correct 1020 c.Assert(timeCalls, Equals, 61) 1021 1022 // should have called cloud-init status 60 times 1023 calls := make([][]string, 60) 1024 for i := 0; i < 60; i++ { 1025 calls[i] = []string{"cloud-init", "status"} 1026 } 1027 1028 c.Assert(cmd.Calls(), DeepEquals, calls) 1029 1030 // now disable should have been called 1031 c.Assert(restrictCalls, Equals, 1) 1032 1033 // now a message after we timeout waiting for the transition 1034 c.Assert(strings.TrimSpace(s.logbuf.String()), Matches, `.*System initialized, cloud-init failed to transition to done or error state after 5 minutes, disabled permanently.*`) 1035 } 1036 1037 func (s *cloudInitSuite) TestCloudInitErrorOnceAllowsFixing(c *C) { 1038 // the absence of a zzzz_snapd.cfg file will indicate that it has not been 1039 // restricted yet and thus it should then check to see if it was manually 1040 // disabled 1041 1042 // the absence of a cloud-init.disabled file indicates that cloud-init is 1043 // "untriggered", i.e. not active/running but could still be triggered 1044 1045 // we use a file to make the mocked cloud-init act differently depending on 1046 // how many times it is called 1047 // this is because we want to test settle()/EnsureBefore() automatically 1048 // re-triggering the EnsureCloudInitRestricted() w/o changing the script 1049 // mid-way through the test while settle() is running 1050 cloudInitScriptStateFile := filepath.Join(c.MkDir(), "cloud-init-state") 1051 1052 cmd := testutil.MockCommand(c, "cloud-init", fmt.Sprintf(` 1053 # the first time the script is called the file shouldn't exist, so return error 1054 # next time when the file exists, return done 1055 if [ -f %[1]s ]; then 1056 status="done" 1057 else 1058 status="error" 1059 touch %[1]s 1060 fi 1061 if [ "$1" = "status" ]; then 1062 echo "status: $status" 1063 else 1064 echo "unexpected args $*" 1065 exit 1 1066 fi`, cloudInitScriptStateFile)) 1067 defer cmd.Restore() 1068 1069 restrictCalls := 0 1070 1071 r := devicestate.MockRestrictCloudInit(func(state sysconfig.CloudInitState, opts *sysconfig.CloudInitRestrictOptions) (sysconfig.CloudInitRestrictionResult, error) { 1072 restrictCalls++ 1073 c.Assert(state, Equals, sysconfig.CloudInitDone) 1074 c.Assert(opts, DeepEquals, &sysconfig.CloudInitRestrictOptions{ 1075 ForceDisable: false, 1076 }) 1077 // we would have restricted it 1078 return sysconfig.CloudInitRestrictionResult{ 1079 Action: "restrict", 1080 // pretend it was NoCloud 1081 DataSource: "NoCloud", 1082 }, nil 1083 }) 1084 defer r() 1085 1086 timeCalls := 0 1087 testStart := time.Now() 1088 r = devicestate.MockTimeNow(func() time.Time { 1089 // we should only call time.Now() twice, first to initialize/set the 1090 // that we saw cloud-init in error, and another immediately after to 1091 // check if the 3 minute timeout has elapsed 1092 timeCalls++ 1093 switch timeCalls { 1094 case 1, 2: 1095 return testStart 1096 default: 1097 c.Errorf("unexpected additional call (number %d) to time.Now()", timeCalls) 1098 return time.Time{} 1099 } 1100 }) 1101 defer r() 1102 1103 err := devicestate.EnsureCloudInitRestricted(s.mgr) 1104 c.Assert(err, IsNil) 1105 1106 // should not have called to restrict 1107 c.Assert(restrictCalls, Equals, 0) 1108 1109 // make sure our time accounting is still correct 1110 c.Assert(timeCalls, Equals, 2) 1111 1112 // only one call to cloud-init status 1113 c.Assert(cmd.Calls(), DeepEquals, [][]string{ 1114 {"cloud-init", "status"}, 1115 }) 1116 1117 // a message about being in error 1118 c.Assert(strings.TrimSpace(s.logbuf.String()), Matches, `.*System initialized, cloud-init reported to be in error state, will disable in 3 minutes`) 1119 s.logbuf.Reset() 1120 1121 // we should have had a call to EnsureBefore, so if we now settle, we will 1122 // see an additional call to cloud-init status, which now returns done and 1123 // progresses 1124 s.settle(c) 1125 1126 c.Assert(cmd.Calls(), DeepEquals, [][]string{ 1127 {"cloud-init", "status"}, 1128 {"cloud-init", "status"}, 1129 }) 1130 1131 // make sure our time accounting is still correct 1132 c.Assert(timeCalls, Equals, 2) 1133 1134 // now restrict should have been called 1135 c.Assert(restrictCalls, Equals, 1) 1136 1137 // we now have a message about restricting 1138 c.Assert(strings.TrimSpace(s.logbuf.String()), Matches, `.*System initialized, cloud-init reported to be done, set datasource_list to \[ NoCloud \] and disabled auto-import by filesystem label`) 1139 } 1140 func (s *cloudInitSuite) TestCloudInitHappyNotFound(c *C) { 1141 // pretend that cloud-init was not found on PATH 1142 statusCalls := 0 1143 r := devicestate.MockCloudInitStatus(func() (sysconfig.CloudInitState, error) { 1144 statusCalls++ 1145 return sysconfig.CloudInitNotFound, nil 1146 }) 1147 defer r() 1148 1149 restrictCalls := 0 1150 r = devicestate.MockRestrictCloudInit(func(state sysconfig.CloudInitState, opts *sysconfig.CloudInitRestrictOptions) (sysconfig.CloudInitRestrictionResult, error) { 1151 restrictCalls++ 1152 // there was no cloud-init binary, so we explicitly disabled it 1153 // if it reappears in future 1154 return sysconfig.CloudInitRestrictionResult{ 1155 Action: "disable", 1156 }, nil 1157 }) 1158 defer r() 1159 1160 err := devicestate.EnsureCloudInitRestricted(s.mgr) 1161 c.Assert(err, IsNil) 1162 c.Assert(statusCalls, Equals, 1) 1163 c.Assert(restrictCalls, Equals, 1) 1164 c.Assert(strings.TrimSpace(s.logbuf.String()), Matches, `.*System initialized, cloud-init not found, disabled permanently`) 1165 }