github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/sysconfig/cloudinit_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 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 sysconfig_test 21 22 import ( 23 "fmt" 24 "io/ioutil" 25 "os" 26 "path/filepath" 27 "testing" 28 29 . "gopkg.in/check.v1" 30 31 "github.com/snapcore/snapd/boot" 32 "github.com/snapcore/snapd/dirs" 33 "github.com/snapcore/snapd/sysconfig" 34 "github.com/snapcore/snapd/testutil" 35 ) 36 37 // Hook up check.v1 into the "go test" runner 38 func Test(t *testing.T) { TestingT(t) } 39 40 type sysconfigSuite struct { 41 testutil.BaseTest 42 43 tmpdir string 44 } 45 46 var _ = Suite(&sysconfigSuite{}) 47 48 func (s *sysconfigSuite) SetUpTest(c *C) { 49 s.BaseTest.SetUpTest(c) 50 51 s.tmpdir = c.MkDir() 52 dirs.SetRootDir(s.tmpdir) 53 s.AddCleanup(func() { dirs.SetRootDir("/") }) 54 } 55 56 func (s *sysconfigSuite) makeCloudCfgSrcDirFiles(c *C) string { 57 cloudCfgSrcDir := c.MkDir() 58 for _, mockCfg := range []string{"foo.cfg", "bar.cfg"} { 59 err := ioutil.WriteFile(filepath.Join(cloudCfgSrcDir, mockCfg), []byte(fmt.Sprintf("%s config", mockCfg)), 0644) 60 c.Assert(err, IsNil) 61 } 62 return cloudCfgSrcDir 63 } 64 65 func (s *sysconfigSuite) makeGadgetCloudConfFile(c *C) string { 66 gadgetDir := c.MkDir() 67 gadgetCloudConf := filepath.Join(gadgetDir, "cloud.conf") 68 err := ioutil.WriteFile(gadgetCloudConf, []byte("gadget cloud config"), 0644) 69 c.Assert(err, IsNil) 70 71 return gadgetDir 72 } 73 74 func (s *sysconfigSuite) TestHasGadgetCloudConf(c *C) { 75 // no cloud.conf is false 76 c.Assert(sysconfig.HasGadgetCloudConf("non-existent-dir-place"), Equals, false) 77 78 // the dir is not enough 79 gadgetDir := c.MkDir() 80 c.Assert(sysconfig.HasGadgetCloudConf(gadgetDir), Equals, false) 81 82 // creating one now is true 83 gadgetCloudConf := filepath.Join(gadgetDir, "cloud.conf") 84 err := ioutil.WriteFile(gadgetCloudConf, []byte("gadget cloud config"), 0644) 85 c.Assert(err, IsNil) 86 87 c.Assert(sysconfig.HasGadgetCloudConf(gadgetDir), Equals, true) 88 } 89 90 // this test is for initramfs calls that disable cloud-init for the ephemeral 91 // writable partition that is used while running during install or recover mode 92 func (s *sysconfigSuite) TestEphemeralModeInitramfsCloudInitDisables(c *C) { 93 writableDefaultsDir := sysconfig.WritableDefaultsDir(boot.InitramfsWritableDir) 94 err := sysconfig.DisableCloudInit(writableDefaultsDir) 95 c.Assert(err, IsNil) 96 97 ubuntuDataCloudDisabled := filepath.Join(boot.InitramfsWritableDir, "_writable_defaults/etc/cloud/cloud-init.disabled") 98 c.Check(ubuntuDataCloudDisabled, testutil.FilePresent) 99 } 100 101 func (s *sysconfigSuite) TestInstallModeCloudInitDisablesByDefaultRunMode(c *C) { 102 err := sysconfig.ConfigureTargetSystem(&sysconfig.Options{ 103 TargetRootDir: boot.InstallHostWritableDir, 104 }) 105 c.Assert(err, IsNil) 106 107 ubuntuDataCloudDisabled := filepath.Join(boot.InstallHostWritableDir, "_writable_defaults/etc/cloud/cloud-init.disabled") 108 c.Check(ubuntuDataCloudDisabled, testutil.FilePresent) 109 } 110 111 func (s *sysconfigSuite) TestInstallModeCloudInitDisallowedIgnoresOtherOptions(c *C) { 112 cloudCfgSrcDir := s.makeCloudCfgSrcDirFiles(c) 113 gadgetDir := s.makeGadgetCloudConfFile(c) 114 115 err := sysconfig.ConfigureTargetSystem(&sysconfig.Options{ 116 AllowCloudInit: false, 117 CloudInitSrcDir: cloudCfgSrcDir, 118 GadgetDir: gadgetDir, 119 TargetRootDir: boot.InstallHostWritableDir, 120 }) 121 c.Assert(err, IsNil) 122 123 ubuntuDataCloudDisabled := filepath.Join(boot.InstallHostWritableDir, "_writable_defaults/etc/cloud/cloud-init.disabled") 124 c.Check(ubuntuDataCloudDisabled, testutil.FilePresent) 125 126 // did not copy ubuntu-seed src files 127 ubuntuDataCloudCfg := filepath.Join(boot.InstallHostWritableDir, "_writable_defaults/etc/cloud/cloud.cfg.d/") 128 c.Check(filepath.Join(ubuntuDataCloudCfg, "foo.cfg"), testutil.FileAbsent) 129 c.Check(filepath.Join(ubuntuDataCloudCfg, "bar.cfg"), testutil.FileAbsent) 130 131 // also did not copy gadget cloud.conf 132 c.Check(filepath.Join(ubuntuDataCloudCfg, "80_device_gadget.cfg"), testutil.FileAbsent) 133 } 134 135 func (s *sysconfigSuite) TestInstallModeCloudInitAllowedDoesNotDisable(c *C) { 136 err := sysconfig.ConfigureTargetSystem(&sysconfig.Options{ 137 AllowCloudInit: true, 138 TargetRootDir: boot.InstallHostWritableDir, 139 }) 140 c.Assert(err, IsNil) 141 142 ubuntuDataCloudDisabled := filepath.Join(boot.InstallHostWritableDir, "_writable_defaults/etc/cloud/cloud-init.disabled") 143 c.Check(ubuntuDataCloudDisabled, testutil.FileAbsent) 144 } 145 146 // this test is the same as the logic from install mode devicestate, where we 147 // want to install cloud-init configuration not onto the running, ephemeral 148 // writable, but rather the host writable partition that will be used upon 149 // reboot into run mode 150 func (s *sysconfigSuite) TestInstallModeCloudInitInstallsOntoHostRunMode(c *C) { 151 cloudCfgSrcDir := s.makeCloudCfgSrcDirFiles(c) 152 153 err := sysconfig.ConfigureTargetSystem(&sysconfig.Options{ 154 AllowCloudInit: true, 155 CloudInitSrcDir: cloudCfgSrcDir, 156 TargetRootDir: boot.InstallHostWritableDir, 157 }) 158 c.Assert(err, IsNil) 159 160 // and did copy the cloud-init files 161 ubuntuDataCloudCfg := filepath.Join(boot.InstallHostWritableDir, "_writable_defaults/etc/cloud/cloud.cfg.d/") 162 c.Check(filepath.Join(ubuntuDataCloudCfg, "foo.cfg"), testutil.FileEquals, "foo.cfg config") 163 c.Check(filepath.Join(ubuntuDataCloudCfg, "bar.cfg"), testutil.FileEquals, "bar.cfg config") 164 } 165 166 func (s *sysconfigSuite) TestInstallModeCloudInitInstallsOntoHostRunModeWithGadgetCloudConf(c *C) { 167 gadgetDir := s.makeGadgetCloudConfFile(c) 168 err := sysconfig.ConfigureTargetSystem(&sysconfig.Options{ 169 AllowCloudInit: true, 170 GadgetDir: gadgetDir, 171 TargetRootDir: boot.InstallHostWritableDir, 172 }) 173 c.Assert(err, IsNil) 174 175 // and did copy the gadget cloud-init file 176 ubuntuDataCloudCfg := filepath.Join(boot.InstallHostWritableDir, "_writable_defaults/etc/cloud/cloud.cfg.d/") 177 c.Check(filepath.Join(ubuntuDataCloudCfg, "80_device_gadget.cfg"), testutil.FileEquals, "gadget cloud config") 178 } 179 180 func (s *sysconfigSuite) TestInstallModeCloudInitInstallsOntoHostRunModeWithGadgetCloudConfIgnoresUbuntuSeedConfig(c *C) { 181 cloudCfgSrcDir := s.makeCloudCfgSrcDirFiles(c) 182 gadgetDir := s.makeGadgetCloudConfFile(c) 183 184 err := sysconfig.ConfigureTargetSystem(&sysconfig.Options{ 185 AllowCloudInit: true, 186 CloudInitSrcDir: cloudCfgSrcDir, 187 GadgetDir: gadgetDir, 188 TargetRootDir: boot.InstallHostWritableDir, 189 }) 190 c.Assert(err, IsNil) 191 192 // and did copy the gadget cloud-init file 193 ubuntuDataCloudCfg := filepath.Join(boot.InstallHostWritableDir, "_writable_defaults/etc/cloud/cloud.cfg.d/") 194 c.Check(filepath.Join(ubuntuDataCloudCfg, "80_device_gadget.cfg"), testutil.FileEquals, "gadget cloud config") 195 196 // but did not copy the ubuntu-seed files 197 c.Check(filepath.Join(ubuntuDataCloudCfg, "foo.cfg"), testutil.FileAbsent) 198 c.Check(filepath.Join(ubuntuDataCloudCfg, "bar.cfg"), testutil.FileAbsent) 199 } 200 201 func (s *sysconfigSuite) TestCloudInitStatusUnhappy(c *C) { 202 cmd := testutil.MockCommand(c, "cloud-init", ` 203 echo cloud-init borken 204 exit 1 205 `) 206 207 status, err := sysconfig.CloudInitStatus() 208 c.Assert(err, ErrorMatches, "cloud-init borken") 209 c.Assert(status, Equals, sysconfig.CloudInitErrored) 210 c.Assert(cmd.Calls(), DeepEquals, [][]string{ 211 {"cloud-init", "status"}, 212 }) 213 } 214 215 func (s *sysconfigSuite) TestCloudInitStatus(c *C) { 216 tt := []struct { 217 comment string 218 cloudInitOutput string 219 exp sysconfig.CloudInitState 220 restrictedFile bool 221 disabledFile bool 222 expError string 223 }{ 224 { 225 comment: "done", 226 cloudInitOutput: "status: done", 227 exp: sysconfig.CloudInitDone, 228 }, 229 { 230 comment: "running", 231 cloudInitOutput: "status: running", 232 exp: sysconfig.CloudInitEnabled, 233 }, 234 { 235 comment: "not run", 236 cloudInitOutput: "status: not run", 237 exp: sysconfig.CloudInitEnabled, 238 }, 239 { 240 comment: "new unrecognized state", 241 cloudInitOutput: "status: newfangledstatus", 242 exp: sysconfig.CloudInitEnabled, 243 }, 244 { 245 comment: "restricted by snapd", 246 restrictedFile: true, 247 exp: sysconfig.CloudInitRestrictedBySnapd, 248 }, 249 { 250 comment: "disabled temporarily", 251 cloudInitOutput: "status: disabled", 252 exp: sysconfig.CloudInitUntriggered, 253 }, 254 { 255 comment: "disabled permanently via file", 256 disabledFile: true, 257 exp: sysconfig.CloudInitDisabledPermanently, 258 }, 259 { 260 comment: "errored", 261 cloudInitOutput: "status: error", 262 exp: sysconfig.CloudInitErrored, 263 }, 264 { 265 comment: "broken cloud-init output", 266 cloudInitOutput: "broken cloud-init output", 267 expError: "invalid cloud-init output: broken cloud-init output", 268 }, 269 } 270 271 for _, t := range tt { 272 old := dirs.GlobalRootDir 273 dirs.SetRootDir(c.MkDir()) 274 defer func() { dirs.SetRootDir(old) }() 275 cmd := testutil.MockCommand(c, "cloud-init", fmt.Sprintf(` 276 if [ "$1" = "status" ]; then 277 echo '%s' 278 else 279 echo "unexpected args, $" 280 exit 1 281 fi 282 `, t.cloudInitOutput)) 283 284 if t.disabledFile { 285 cloudDir := filepath.Join(dirs.GlobalRootDir, "etc/cloud") 286 err := os.MkdirAll(cloudDir, 0755) 287 c.Assert(err, IsNil) 288 err = ioutil.WriteFile(filepath.Join(cloudDir, "cloud-init.disabled"), nil, 0644) 289 c.Assert(err, IsNil) 290 } 291 292 if t.restrictedFile { 293 cloudDir := filepath.Join(dirs.GlobalRootDir, "etc/cloud/cloud.cfg.d") 294 err := os.MkdirAll(cloudDir, 0755) 295 c.Assert(err, IsNil) 296 err = ioutil.WriteFile(filepath.Join(cloudDir, "zzzz_snapd.cfg"), nil, 0644) 297 c.Assert(err, IsNil) 298 } 299 300 status, err := sysconfig.CloudInitStatus() 301 if t.expError != "" { 302 c.Assert(err, ErrorMatches, t.expError, Commentf(t.comment)) 303 } else { 304 c.Assert(err, IsNil) 305 c.Assert(status, Equals, t.exp, Commentf(t.comment)) 306 } 307 308 // if the restricted file was there we don't call cloud-init status 309 var expCalls [][]string 310 if !t.restrictedFile && !t.disabledFile { 311 expCalls = [][]string{ 312 {"cloud-init", "status"}, 313 } 314 } 315 316 c.Assert(cmd.Calls(), DeepEquals, expCalls, Commentf(t.comment)) 317 cmd.Restore() 318 } 319 } 320 321 func (s *sysconfigSuite) TestCloudInitNotFoundStatus(c *C) { 322 emptyDir := c.MkDir() 323 oldPath := os.Getenv("PATH") 324 defer func() { 325 c.Assert(os.Setenv("PATH", oldPath), IsNil) 326 }() 327 os.Setenv("PATH", emptyDir) 328 329 status, err := sysconfig.CloudInitStatus() 330 c.Assert(err, IsNil) 331 c.Check(status, Equals, sysconfig.CloudInitNotFound) 332 } 333 334 var gceCloudInitStatusJSON = `{ 335 "v1": { 336 "datasource": "DataSourceGCE", 337 "init": { 338 "errors": [], 339 "finished": 1591751113.4536479, 340 "start": 1591751112.130069 341 }, 342 "stage": null 343 } 344 } 345 ` 346 347 var multipassNoCloudCloudInitStatusJSON = `{ 348 "v1": { 349 "datasource": "DataSourceNoCloud [seed=/dev/sr0][dsmode=net]", 350 "init": { 351 "errors": [], 352 "finished": 1591788514.4656117, 353 "start": 1591788514.2607572 354 }, 355 "stage": null 356 } 357 }` 358 359 var localNoneCloudInitStatusJSON = `{ 360 "v1": { 361 "datasource": "DataSourceNone", 362 "init": { 363 "errors": [], 364 "finished": 1591788514.4656117, 365 "start": 1591788514.2607572 366 }, 367 "stage": null 368 } 369 }` 370 371 var lxdNoCloudCloudInitStatusJSON = `{ 372 "v1": { 373 "datasource": "DataSourceNoCloud [seed=/var/lib/cloud/seed/nocloud-net][dsmode=net]", 374 "init": { 375 "errors": [], 376 "finished": 1591788737.3982718, 377 "start": 1591788736.9015596 378 }, 379 "stage": null 380 } 381 }` 382 383 var restrictNoCloudYaml = `datasource_list: [NoCloud] 384 datasource: 385 NoCloud: 386 fs_label: null 387 manual_cache_clean: true 388 ` 389 390 func (s *sysconfigSuite) TestRestrictCloudInit(c *C) { 391 tt := []struct { 392 comment string 393 state sysconfig.CloudInitState 394 sysconfOpts *sysconfig.CloudInitRestrictOptions 395 cloudInitStatusJSON string 396 expError string 397 expRestrictYamlWritten string 398 expDatasource string 399 expAction string 400 expDisableFile bool 401 }{ 402 { 403 comment: "already disabled", 404 state: sysconfig.CloudInitDisabledPermanently, 405 expError: "cannot restrict cloud-init: already disabled", 406 }, 407 { 408 comment: "already restricted", 409 state: sysconfig.CloudInitRestrictedBySnapd, 410 expError: "cannot restrict cloud-init: already restricted", 411 }, 412 { 413 comment: "errored", 414 state: sysconfig.CloudInitErrored, 415 expError: "cannot restrict cloud-init in error or enabled state", 416 }, 417 { 418 comment: "enable (not running)", 419 state: sysconfig.CloudInitEnabled, 420 expError: "cannot restrict cloud-init in error or enabled state", 421 }, 422 { 423 comment: "errored w/ force disable", 424 state: sysconfig.CloudInitErrored, 425 sysconfOpts: &sysconfig.CloudInitRestrictOptions{ 426 ForceDisable: true, 427 }, 428 expAction: "disable", 429 expDisableFile: true, 430 }, 431 { 432 comment: "enable (not running) w/ force disable", 433 state: sysconfig.CloudInitEnabled, 434 sysconfOpts: &sysconfig.CloudInitRestrictOptions{ 435 ForceDisable: true, 436 }, 437 expAction: "disable", 438 expDisableFile: true, 439 }, 440 { 441 comment: "untriggered", 442 state: sysconfig.CloudInitUntriggered, 443 expAction: "disable", 444 expDisableFile: true, 445 }, 446 { 447 comment: "unknown status", 448 state: -1, 449 expAction: "disable", 450 expDisableFile: true, 451 }, 452 { 453 comment: "gce done", 454 state: sysconfig.CloudInitDone, 455 cloudInitStatusJSON: gceCloudInitStatusJSON, 456 expDatasource: "GCE", 457 expAction: "restrict", 458 expRestrictYamlWritten: `datasource_list: [GCE] 459 `, 460 }, 461 { 462 comment: "nocloud done", 463 state: sysconfig.CloudInitDone, 464 cloudInitStatusJSON: multipassNoCloudCloudInitStatusJSON, 465 expDatasource: "NoCloud", 466 expAction: "restrict", 467 expRestrictYamlWritten: restrictNoCloudYaml, 468 }, 469 { 470 comment: "nocloud uc20 done", 471 state: sysconfig.CloudInitDone, 472 cloudInitStatusJSON: multipassNoCloudCloudInitStatusJSON, 473 sysconfOpts: &sysconfig.CloudInitRestrictOptions{ 474 DisableAfterLocalDatasourcesRun: true, 475 }, 476 expDatasource: "NoCloud", 477 expAction: "disable", 478 expDisableFile: true, 479 }, 480 { 481 comment: "none uc20 done", 482 state: sysconfig.CloudInitDone, 483 cloudInitStatusJSON: localNoneCloudInitStatusJSON, 484 sysconfOpts: &sysconfig.CloudInitRestrictOptions{ 485 DisableAfterLocalDatasourcesRun: true, 486 }, 487 expDatasource: "None", 488 expAction: "disable", 489 expDisableFile: true, 490 }, 491 492 // the two cases for lxd and multipass are effectively the same, but as 493 // the largest known users of cloud-init w/ UC, we leave them as 494 // separate test cases for their different cloud-init status.json 495 // content 496 { 497 comment: "nocloud multipass done", 498 state: sysconfig.CloudInitDone, 499 cloudInitStatusJSON: multipassNoCloudCloudInitStatusJSON, 500 expDatasource: "NoCloud", 501 expAction: "restrict", 502 expRestrictYamlWritten: restrictNoCloudYaml, 503 }, 504 { 505 comment: "nocloud seed lxd done", 506 state: sysconfig.CloudInitDone, 507 cloudInitStatusJSON: lxdNoCloudCloudInitStatusJSON, 508 expDatasource: "NoCloud", 509 expAction: "restrict", 510 expRestrictYamlWritten: restrictNoCloudYaml, 511 }, 512 { 513 comment: "nocloud uc20 multipass done", 514 state: sysconfig.CloudInitDone, 515 cloudInitStatusJSON: multipassNoCloudCloudInitStatusJSON, 516 sysconfOpts: &sysconfig.CloudInitRestrictOptions{ 517 DisableAfterLocalDatasourcesRun: true, 518 }, 519 expDatasource: "NoCloud", 520 expAction: "disable", 521 expDisableFile: true, 522 }, 523 { 524 comment: "nocloud uc20 seed lxd done", 525 state: sysconfig.CloudInitDone, 526 cloudInitStatusJSON: lxdNoCloudCloudInitStatusJSON, 527 sysconfOpts: &sysconfig.CloudInitRestrictOptions{ 528 DisableAfterLocalDatasourcesRun: true, 529 }, 530 expDatasource: "NoCloud", 531 expAction: "disable", 532 expDisableFile: true, 533 }, 534 { 535 comment: "no cloud-init in $PATH", 536 state: sysconfig.CloudInitNotFound, 537 expAction: "disable", 538 expDisableFile: true, 539 }, 540 } 541 542 for _, t := range tt { 543 comment := Commentf("%s", t.comment) 544 // setup status.json 545 old := dirs.GlobalRootDir 546 dirs.SetRootDir(c.MkDir()) 547 defer func() { dirs.SetRootDir(old) }() 548 statusJSONFile := filepath.Join(dirs.GlobalRootDir, "/run/cloud-init/status.json") 549 if t.cloudInitStatusJSON != "" { 550 err := os.MkdirAll(filepath.Dir(statusJSONFile), 0755) 551 c.Assert(err, IsNil, comment) 552 err = ioutil.WriteFile(statusJSONFile, []byte(t.cloudInitStatusJSON), 0644) 553 c.Assert(err, IsNil, comment) 554 } 555 556 // if we expect snapd to write a yaml config file for cloud-init, ensure 557 // the dir exists before hand 558 if t.expRestrictYamlWritten != "" { 559 err := os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "/etc/cloud/cloud.cfg.d"), 0755) 560 c.Assert(err, IsNil, comment) 561 } 562 563 res, err := sysconfig.RestrictCloudInit(t.state, t.sysconfOpts) 564 if t.expError == "" { 565 c.Assert(err, IsNil, comment) 566 c.Assert(res.DataSource, Equals, t.expDatasource, comment) 567 c.Assert(res.Action, Equals, t.expAction, comment) 568 if t.expRestrictYamlWritten != "" { 569 // check the snapd restrict yaml file that should have been written 570 c.Assert( 571 filepath.Join(dirs.GlobalRootDir, "/etc/cloud/cloud.cfg.d/zzzz_snapd.cfg"), 572 testutil.FileEquals, 573 t.expRestrictYamlWritten, 574 comment, 575 ) 576 } 577 578 // if we expect the disable file to be written then check for it 579 // otherwise ensure it was not written accidentally 580 var fileCheck Checker 581 if t.expDisableFile { 582 fileCheck = testutil.FilePresent 583 } else { 584 fileCheck = testutil.FileAbsent 585 } 586 587 c.Assert( 588 filepath.Join(dirs.GlobalRootDir, "/etc/cloud/cloud-init.disabled"), 589 fileCheck, 590 comment, 591 ) 592 593 } else { 594 c.Assert(err, ErrorMatches, t.expError, comment) 595 } 596 } 597 }