github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/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 var gceCloudInitStatusJSON = `{ 322 "v1": { 323 "datasource": "DataSourceGCE", 324 "init": { 325 "errors": [], 326 "finished": 1591751113.4536479, 327 "start": 1591751112.130069 328 }, 329 "stage": null 330 } 331 } 332 ` 333 334 var multipassNoCloudCloudInitStatusJSON = `{ 335 "v1": { 336 "datasource": "DataSourceNoCloud [seed=/dev/sr0][dsmode=net]", 337 "init": { 338 "errors": [], 339 "finished": 1591788514.4656117, 340 "start": 1591788514.2607572 341 }, 342 "stage": null 343 } 344 }` 345 346 var localNoneCloudInitStatusJSON = `{ 347 "v1": { 348 "datasource": "DataSourceNone", 349 "init": { 350 "errors": [], 351 "finished": 1591788514.4656117, 352 "start": 1591788514.2607572 353 }, 354 "stage": null 355 } 356 }` 357 358 var lxdNoCloudCloudInitStatusJSON = `{ 359 "v1": { 360 "datasource": "DataSourceNoCloud [seed=/var/lib/cloud/seed/nocloud-net][dsmode=net]", 361 "init": { 362 "errors": [], 363 "finished": 1591788737.3982718, 364 "start": 1591788736.9015596 365 }, 366 "stage": null 367 } 368 }` 369 370 var restrictNoCloudYaml = `datasource_list: [NoCloud] 371 datasource: 372 NoCloud: 373 fs_label: null 374 manual_cache_clean: true 375 ` 376 377 func (s *sysconfigSuite) TestRestrictCloudInit(c *C) { 378 tt := []struct { 379 comment string 380 state sysconfig.CloudInitState 381 sysconfOpts *sysconfig.CloudInitRestrictOptions 382 cloudInitStatusJSON string 383 expError string 384 expRestrictYamlWritten string 385 expDatasource string 386 expAction string 387 expDisableFile bool 388 }{ 389 { 390 comment: "already disabled", 391 state: sysconfig.CloudInitDisabledPermanently, 392 expError: "cannot restrict cloud-init: already disabled", 393 }, 394 { 395 comment: "already restricted", 396 state: sysconfig.CloudInitRestrictedBySnapd, 397 expError: "cannot restrict cloud-init: already restricted", 398 }, 399 { 400 comment: "errored", 401 state: sysconfig.CloudInitErrored, 402 expError: "cannot restrict cloud-init in error or enabled state", 403 }, 404 { 405 comment: "enable (not running)", 406 state: sysconfig.CloudInitEnabled, 407 expError: "cannot restrict cloud-init in error or enabled state", 408 }, 409 { 410 comment: "errored w/ force disable", 411 state: sysconfig.CloudInitErrored, 412 sysconfOpts: &sysconfig.CloudInitRestrictOptions{ 413 ForceDisable: true, 414 }, 415 expAction: "disable", 416 expDisableFile: true, 417 }, 418 { 419 comment: "enable (not running) w/ force disable", 420 state: sysconfig.CloudInitEnabled, 421 sysconfOpts: &sysconfig.CloudInitRestrictOptions{ 422 ForceDisable: true, 423 }, 424 expAction: "disable", 425 expDisableFile: true, 426 }, 427 { 428 comment: "untriggered", 429 state: sysconfig.CloudInitUntriggered, 430 expAction: "disable", 431 expDisableFile: true, 432 }, 433 { 434 comment: "unknown status", 435 state: -1, 436 expAction: "disable", 437 expDisableFile: true, 438 }, 439 { 440 comment: "gce done", 441 state: sysconfig.CloudInitDone, 442 cloudInitStatusJSON: gceCloudInitStatusJSON, 443 expDatasource: "GCE", 444 expAction: "restrict", 445 expRestrictYamlWritten: `datasource_list: [GCE] 446 `, 447 }, 448 { 449 comment: "nocloud done", 450 state: sysconfig.CloudInitDone, 451 cloudInitStatusJSON: multipassNoCloudCloudInitStatusJSON, 452 expDatasource: "NoCloud", 453 expAction: "restrict", 454 expRestrictYamlWritten: restrictNoCloudYaml, 455 }, 456 { 457 comment: "nocloud uc20 done", 458 state: sysconfig.CloudInitDone, 459 cloudInitStatusJSON: multipassNoCloudCloudInitStatusJSON, 460 sysconfOpts: &sysconfig.CloudInitRestrictOptions{ 461 DisableAfterLocalDatasourcesRun: true, 462 }, 463 expDatasource: "NoCloud", 464 expAction: "disable", 465 expDisableFile: true, 466 }, 467 { 468 comment: "none uc20 done", 469 state: sysconfig.CloudInitDone, 470 cloudInitStatusJSON: localNoneCloudInitStatusJSON, 471 sysconfOpts: &sysconfig.CloudInitRestrictOptions{ 472 DisableAfterLocalDatasourcesRun: true, 473 }, 474 expDatasource: "None", 475 expAction: "disable", 476 expDisableFile: true, 477 }, 478 479 // the two cases for lxd and multipass are effectively the same, but as 480 // the largest known users of cloud-init w/ UC, we leave them as 481 // separate test cases for their different cloud-init status.json 482 // content 483 { 484 comment: "nocloud multipass done", 485 state: sysconfig.CloudInitDone, 486 cloudInitStatusJSON: multipassNoCloudCloudInitStatusJSON, 487 expDatasource: "NoCloud", 488 expAction: "restrict", 489 expRestrictYamlWritten: restrictNoCloudYaml, 490 }, 491 { 492 comment: "nocloud seed lxd done", 493 state: sysconfig.CloudInitDone, 494 cloudInitStatusJSON: lxdNoCloudCloudInitStatusJSON, 495 expDatasource: "NoCloud", 496 expAction: "restrict", 497 expRestrictYamlWritten: restrictNoCloudYaml, 498 }, 499 { 500 comment: "nocloud uc20 multipass done", 501 state: sysconfig.CloudInitDone, 502 cloudInitStatusJSON: multipassNoCloudCloudInitStatusJSON, 503 sysconfOpts: &sysconfig.CloudInitRestrictOptions{ 504 DisableAfterLocalDatasourcesRun: true, 505 }, 506 expDatasource: "NoCloud", 507 expAction: "disable", 508 expDisableFile: true, 509 }, 510 { 511 comment: "nocloud uc20 seed lxd done", 512 state: sysconfig.CloudInitDone, 513 cloudInitStatusJSON: lxdNoCloudCloudInitStatusJSON, 514 sysconfOpts: &sysconfig.CloudInitRestrictOptions{ 515 DisableAfterLocalDatasourcesRun: true, 516 }, 517 expDatasource: "NoCloud", 518 expAction: "disable", 519 expDisableFile: true, 520 }, 521 } 522 523 for _, t := range tt { 524 comment := Commentf("%s", t.comment) 525 // setup status.json 526 old := dirs.GlobalRootDir 527 dirs.SetRootDir(c.MkDir()) 528 defer func() { dirs.SetRootDir(old) }() 529 statusJSONFile := filepath.Join(dirs.GlobalRootDir, "/run/cloud-init/status.json") 530 if t.cloudInitStatusJSON != "" { 531 err := os.MkdirAll(filepath.Dir(statusJSONFile), 0755) 532 c.Assert(err, IsNil, comment) 533 err = ioutil.WriteFile(statusJSONFile, []byte(t.cloudInitStatusJSON), 0644) 534 c.Assert(err, IsNil, comment) 535 } 536 537 // if we expect snapd to write a yaml config file for cloud-init, ensure 538 // the dir exists before hand 539 if t.expRestrictYamlWritten != "" { 540 err := os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "/etc/cloud/cloud.cfg.d"), 0755) 541 c.Assert(err, IsNil, comment) 542 } 543 544 res, err := sysconfig.RestrictCloudInit(t.state, t.sysconfOpts) 545 if t.expError == "" { 546 c.Assert(err, IsNil, comment) 547 c.Assert(res.DataSource, Equals, t.expDatasource, comment) 548 c.Assert(res.Action, Equals, t.expAction, comment) 549 if t.expRestrictYamlWritten != "" { 550 // check the snapd restrict yaml file that should have been written 551 c.Assert( 552 filepath.Join(dirs.GlobalRootDir, "/etc/cloud/cloud.cfg.d/zzzz_snapd.cfg"), 553 testutil.FileEquals, 554 t.expRestrictYamlWritten, 555 comment, 556 ) 557 } 558 559 // if we expect the disable file to be written then check for it 560 // otherwise ensure it was not written accidentally 561 var fileCheck Checker 562 if t.expDisableFile { 563 fileCheck = testutil.FilePresent 564 } else { 565 fileCheck = testutil.FileAbsent 566 } 567 568 c.Assert( 569 filepath.Join(dirs.GlobalRootDir, "/etc/cloud/cloud-init.disabled"), 570 fileCheck, 571 comment, 572 ) 573 574 } else { 575 c.Assert(err, ErrorMatches, t.expError, comment) 576 } 577 } 578 }