gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/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("#cloud-config 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(fake20Model("signed"), &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(fake20Model("signed"), &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) TestInstallModeCloudInitAllowedGradeSignedDoesNotDisable(c *C) {
   136  	err := sysconfig.ConfigureTargetSystem(fake20Model("signed"), &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  func (s *sysconfigSuite) TestInstallModeCloudInitAllowedGradeSignedDoesNotInstallUbuntuSeedConfig(c *C) {
   147  	cloudCfgSrcDir := s.makeCloudCfgSrcDirFiles(c)
   148  
   149  	err := sysconfig.ConfigureTargetSystem(fake20Model("signed"), &sysconfig.Options{
   150  		AllowCloudInit:  true,
   151  		TargetRootDir:   boot.InstallHostWritableDir,
   152  		CloudInitSrcDir: cloudCfgSrcDir,
   153  	})
   154  	c.Assert(err, IsNil)
   155  
   156  	ubuntuDataCloudCfg := filepath.Join(boot.InstallHostWritableDir, "_writable_defaults/etc/cloud/cloud.cfg.d/")
   157  	c.Check(filepath.Join(ubuntuDataCloudCfg, "foo.cfg"), testutil.FileAbsent)
   158  	c.Check(filepath.Join(ubuntuDataCloudCfg, "bar.cfg"), testutil.FileAbsent)
   159  }
   160  
   161  func (s *sysconfigSuite) TestInstallModeCloudInitAllowedGradeDangerousDoesNotDisable(c *C) {
   162  	err := sysconfig.ConfigureTargetSystem(fake20Model("dangerous"), &sysconfig.Options{
   163  		AllowCloudInit: true,
   164  		TargetRootDir:  boot.InstallHostWritableDir,
   165  	})
   166  	c.Assert(err, IsNil)
   167  
   168  	ubuntuDataCloudDisabled := filepath.Join(boot.InstallHostWritableDir, "_writable_defaults/etc/cloud/cloud-init.disabled")
   169  	c.Check(ubuntuDataCloudDisabled, testutil.FileAbsent)
   170  }
   171  
   172  func (s *sysconfigSuite) TestInstallModeCloudInitDisallowedGradeSecuredDoesDisable(c *C) {
   173  	err := sysconfig.ConfigureTargetSystem(fake20Model("secured"), &sysconfig.Options{
   174  		AllowCloudInit: false,
   175  		TargetRootDir:  boot.InstallHostWritableDir,
   176  	})
   177  	c.Assert(err, IsNil)
   178  
   179  	ubuntuDataCloudDisabled := filepath.Join(boot.InstallHostWritableDir, "_writable_defaults/etc/cloud/cloud-init.disabled")
   180  	c.Check(ubuntuDataCloudDisabled, testutil.FilePresent)
   181  }
   182  
   183  func (s *sysconfigSuite) TestInstallModeCloudInitAllowedGradeSecuredIgnoresSrcButDoesNotDisable(c *C) {
   184  	cloudCfgSrcDir := s.makeCloudCfgSrcDirFiles(c)
   185  
   186  	err := sysconfig.ConfigureTargetSystem(fake20Model("secured"), &sysconfig.Options{
   187  		AllowCloudInit:  true,
   188  		CloudInitSrcDir: cloudCfgSrcDir,
   189  		TargetRootDir:   boot.InstallHostWritableDir,
   190  	})
   191  	c.Assert(err, IsNil)
   192  
   193  	// the disable file is not present
   194  	ubuntuDataCloudDisabled := filepath.Join(boot.InstallHostWritableDir, "_writable_defaults/etc/cloud/cloud-init.disabled")
   195  	c.Check(ubuntuDataCloudDisabled, testutil.FileAbsent)
   196  
   197  	// but we did not copy the config files from ubuntu-seed, even though they
   198  	// are there and cloud-init is not disabled
   199  	ubuntuDataCloudCfg := filepath.Join(boot.InstallHostWritableDir, "_writable_defaults/etc/cloud/cloud.cfg.d/")
   200  	c.Check(filepath.Join(ubuntuDataCloudCfg, "foo.cfg"), testutil.FileAbsent)
   201  	c.Check(filepath.Join(ubuntuDataCloudCfg, "bar.cfg"), testutil.FileAbsent)
   202  }
   203  
   204  // this test is the same as the logic from install mode devicestate, where we
   205  // want to install cloud-init configuration not onto the running, ephemeral
   206  // writable, but rather the host writable partition that will be used upon
   207  // reboot into run mode
   208  func (s *sysconfigSuite) TestInstallModeCloudInitInstallsOntoHostRunMode(c *C) {
   209  	cloudCfgSrcDir := s.makeCloudCfgSrcDirFiles(c)
   210  
   211  	err := sysconfig.ConfigureTargetSystem(fake20Model("dangerous"), &sysconfig.Options{
   212  		AllowCloudInit:  true,
   213  		CloudInitSrcDir: cloudCfgSrcDir,
   214  		TargetRootDir:   boot.InstallHostWritableDir,
   215  	})
   216  	c.Assert(err, IsNil)
   217  
   218  	// and did copy the cloud-init files
   219  	ubuntuDataCloudCfg := filepath.Join(boot.InstallHostWritableDir, "_writable_defaults/etc/cloud/cloud.cfg.d/")
   220  	c.Check(filepath.Join(ubuntuDataCloudCfg, "90_foo.cfg"), testutil.FileEquals, "foo.cfg config")
   221  	c.Check(filepath.Join(ubuntuDataCloudCfg, "90_bar.cfg"), testutil.FileEquals, "bar.cfg config")
   222  }
   223  
   224  func (s *sysconfigSuite) TestInstallModeCloudInitInstallsOntoHostRunModeWithGadgetCloudConf(c *C) {
   225  	gadgetDir := s.makeGadgetCloudConfFile(c)
   226  	err := sysconfig.ConfigureTargetSystem(fake20Model("secured"), &sysconfig.Options{
   227  		AllowCloudInit: true,
   228  		GadgetDir:      gadgetDir,
   229  		TargetRootDir:  boot.InstallHostWritableDir,
   230  	})
   231  	c.Assert(err, IsNil)
   232  
   233  	// and did copy the gadget cloud-init file
   234  	ubuntuDataCloudCfg := filepath.Join(boot.InstallHostWritableDir, "_writable_defaults/etc/cloud/cloud.cfg.d/")
   235  	c.Check(filepath.Join(ubuntuDataCloudCfg, "80_device_gadget.cfg"), testutil.FileEquals, "#cloud-config gadget cloud config")
   236  }
   237  
   238  func (s *sysconfigSuite) TestInstallModeCloudInitInstallsOntoHostRunModeWithGadgetCloudConfAlsoInstallsUbuntuSeedConfig(c *C) {
   239  	cloudCfgSrcDir := s.makeCloudCfgSrcDirFiles(c)
   240  	gadgetDir := s.makeGadgetCloudConfFile(c)
   241  
   242  	err := sysconfig.ConfigureTargetSystem(fake20Model("dangerous"), &sysconfig.Options{
   243  		AllowCloudInit:  true,
   244  		CloudInitSrcDir: cloudCfgSrcDir,
   245  		GadgetDir:       gadgetDir,
   246  		TargetRootDir:   boot.InstallHostWritableDir,
   247  	})
   248  	c.Assert(err, IsNil)
   249  
   250  	// we did copy the gadget cloud-init file
   251  	ubuntuDataCloudCfg := filepath.Join(boot.InstallHostWritableDir, "_writable_defaults/etc/cloud/cloud.cfg.d/")
   252  	c.Check(filepath.Join(ubuntuDataCloudCfg, "80_device_gadget.cfg"), testutil.FileEquals, "#cloud-config gadget cloud config")
   253  
   254  	// and we also copied the ubuntu-seed files with a new prefix such that they
   255  	// take precedence over the gadget file by being ordered lexically after the
   256  	// gadget file
   257  	c.Check(filepath.Join(ubuntuDataCloudCfg, "90_foo.cfg"), testutil.FileEquals, "foo.cfg config")
   258  	c.Check(filepath.Join(ubuntuDataCloudCfg, "90_bar.cfg"), testutil.FileEquals, "bar.cfg config")
   259  }
   260  
   261  func (s *sysconfigSuite) TestCloudInitStatusUnhappy(c *C) {
   262  	cmd := testutil.MockCommand(c, "cloud-init", `
   263  echo cloud-init borken
   264  exit 1
   265  `)
   266  
   267  	status, err := sysconfig.CloudInitStatus()
   268  	c.Assert(err, ErrorMatches, "cloud-init borken")
   269  	c.Assert(status, Equals, sysconfig.CloudInitErrored)
   270  	c.Assert(cmd.Calls(), DeepEquals, [][]string{
   271  		{"cloud-init", "status"},
   272  	})
   273  }
   274  
   275  func (s *sysconfigSuite) TestCloudInitStatus(c *C) {
   276  	tt := []struct {
   277  		comment         string
   278  		cloudInitOutput string
   279  		exitCode        int
   280  		exp             sysconfig.CloudInitState
   281  		restrictedFile  bool
   282  		disabledFile    bool
   283  		expError        string
   284  	}{
   285  		{
   286  			comment:         "done",
   287  			cloudInitOutput: "status: done",
   288  			exp:             sysconfig.CloudInitDone,
   289  		},
   290  		{
   291  			comment:         "running",
   292  			cloudInitOutput: "status: running",
   293  			exp:             sysconfig.CloudInitEnabled,
   294  		},
   295  		{
   296  			comment:         "not run",
   297  			cloudInitOutput: "status: not run",
   298  			exp:             sysconfig.CloudInitEnabled,
   299  		},
   300  		{
   301  			comment:         "new unrecognized state",
   302  			cloudInitOutput: "status: newfangledstatus",
   303  			exp:             sysconfig.CloudInitEnabled,
   304  		},
   305  		{
   306  			comment:        "restricted by snapd",
   307  			restrictedFile: true,
   308  			exp:            sysconfig.CloudInitRestrictedBySnapd,
   309  		},
   310  		{
   311  			comment:         "disabled temporarily",
   312  			cloudInitOutput: "status: disabled",
   313  			exp:             sysconfig.CloudInitUntriggered,
   314  		},
   315  		{
   316  			comment:      "disabled permanently via file",
   317  			disabledFile: true,
   318  			exp:          sysconfig.CloudInitDisabledPermanently,
   319  		},
   320  		{
   321  			comment:         "errored w/ exit code 0",
   322  			cloudInitOutput: "status: error",
   323  			exp:             sysconfig.CloudInitErrored,
   324  			exitCode:        0,
   325  		},
   326  		{
   327  			comment:         "errored w/ exit code 1",
   328  			cloudInitOutput: "status: error",
   329  			exp:             sysconfig.CloudInitErrored,
   330  			exitCode:        1,
   331  		},
   332  		{
   333  			comment:         "broken cloud-init output w/ exit code 0",
   334  			cloudInitOutput: "broken cloud-init output",
   335  			expError:        "invalid cloud-init output: broken cloud-init output",
   336  		},
   337  		{
   338  			comment:         "broken cloud-init output w/ exit code 1",
   339  			cloudInitOutput: "broken cloud-init output",
   340  			exitCode:        1,
   341  			expError:        "broken cloud-init output",
   342  		},
   343  		{
   344  			comment:         "normal cloud-init output w/ exit code 1",
   345  			cloudInitOutput: "status: foobar",
   346  			exitCode:        1,
   347  			expError:        "cloud-init errored: status: foobar",
   348  		},
   349  	}
   350  
   351  	for _, t := range tt {
   352  		old := dirs.GlobalRootDir
   353  		dirs.SetRootDir(c.MkDir())
   354  		defer func() { dirs.SetRootDir(old) }()
   355  		cmd := testutil.MockCommand(c, "cloud-init", fmt.Sprintf(`
   356  if [ "$1" = "status" ]; then
   357  	echo '%s'
   358  	exit %d
   359  else 
   360  	echo "unexpected args, $"
   361  	exit 1
   362  fi
   363  		`, t.cloudInitOutput, t.exitCode))
   364  
   365  		if t.disabledFile {
   366  			cloudDir := filepath.Join(dirs.GlobalRootDir, "etc/cloud")
   367  			err := os.MkdirAll(cloudDir, 0755)
   368  			c.Assert(err, IsNil)
   369  			err = ioutil.WriteFile(filepath.Join(cloudDir, "cloud-init.disabled"), nil, 0644)
   370  			c.Assert(err, IsNil)
   371  		}
   372  
   373  		if t.restrictedFile {
   374  			cloudDir := filepath.Join(dirs.GlobalRootDir, "etc/cloud/cloud.cfg.d")
   375  			err := os.MkdirAll(cloudDir, 0755)
   376  			c.Assert(err, IsNil)
   377  			err = ioutil.WriteFile(filepath.Join(cloudDir, "zzzz_snapd.cfg"), nil, 0644)
   378  			c.Assert(err, IsNil)
   379  		}
   380  
   381  		status, err := sysconfig.CloudInitStatus()
   382  		if t.expError != "" {
   383  			c.Assert(err, ErrorMatches, t.expError, Commentf(t.comment))
   384  		} else {
   385  			c.Assert(err, IsNil)
   386  			c.Assert(status, Equals, t.exp, Commentf(t.comment))
   387  		}
   388  
   389  		// if the restricted file was there we don't call cloud-init status
   390  		var expCalls [][]string
   391  		if !t.restrictedFile && !t.disabledFile {
   392  			expCalls = [][]string{
   393  				{"cloud-init", "status"},
   394  			}
   395  		}
   396  
   397  		c.Assert(cmd.Calls(), DeepEquals, expCalls, Commentf(t.comment))
   398  		cmd.Restore()
   399  	}
   400  }
   401  
   402  func (s *sysconfigSuite) TestCloudInitNotFoundStatus(c *C) {
   403  	emptyDir := c.MkDir()
   404  	oldPath := os.Getenv("PATH")
   405  	defer func() {
   406  		c.Assert(os.Setenv("PATH", oldPath), IsNil)
   407  	}()
   408  	os.Setenv("PATH", emptyDir)
   409  
   410  	status, err := sysconfig.CloudInitStatus()
   411  	c.Assert(err, IsNil)
   412  	c.Check(status, Equals, sysconfig.CloudInitNotFound)
   413  }
   414  
   415  var gceCloudInitStatusJSON = `{
   416  	"v1": {
   417  	 "datasource": "DataSourceGCE",
   418  	 "init": {
   419  	  "errors": [],
   420  	  "finished": 1591751113.4536479,
   421  	  "start": 1591751112.130069
   422  	 },
   423  	 "stage": null
   424  	}
   425  }
   426  `
   427  
   428  var multipassNoCloudCloudInitStatusJSON = `{
   429   "v1": {
   430    "datasource": "DataSourceNoCloud [seed=/dev/sr0][dsmode=net]",
   431    "init": {
   432     "errors": [],
   433     "finished": 1591788514.4656117,
   434     "start": 1591788514.2607572
   435    },
   436    "stage": null
   437   }
   438  }`
   439  
   440  var localNoneCloudInitStatusJSON = `{
   441   "v1": {
   442    "datasource": "DataSourceNone",
   443    "init": {
   444     "errors": [],
   445     "finished": 1591788514.4656117,
   446     "start": 1591788514.2607572
   447    },
   448    "stage": null
   449   }
   450  }`
   451  
   452  var lxdNoCloudCloudInitStatusJSON = `{
   453   "v1": {
   454    "datasource": "DataSourceNoCloud [seed=/var/lib/cloud/seed/nocloud-net][dsmode=net]",
   455    "init": {
   456     "errors": [],
   457     "finished": 1591788737.3982718,
   458     "start": 1591788736.9015596
   459    },
   460    "stage": null
   461   }
   462  }`
   463  
   464  var restrictNoCloudYaml = `datasource_list: [NoCloud]
   465  datasource:
   466    NoCloud:
   467      fs_label: null
   468  manual_cache_clean: true
   469  `
   470  
   471  func (s *sysconfigSuite) TestRestrictCloudInit(c *C) {
   472  	tt := []struct {
   473  		comment                string
   474  		state                  sysconfig.CloudInitState
   475  		sysconfOpts            *sysconfig.CloudInitRestrictOptions
   476  		cloudInitStatusJSON    string
   477  		expError               string
   478  		expRestrictYamlWritten string
   479  		expDatasource          string
   480  		expAction              string
   481  		expDisableFile         bool
   482  	}{
   483  		{
   484  			comment:  "already disabled",
   485  			state:    sysconfig.CloudInitDisabledPermanently,
   486  			expError: "cannot restrict cloud-init: already disabled",
   487  		},
   488  		{
   489  			comment:  "already restricted",
   490  			state:    sysconfig.CloudInitRestrictedBySnapd,
   491  			expError: "cannot restrict cloud-init: already restricted",
   492  		},
   493  		{
   494  			comment:  "errored",
   495  			state:    sysconfig.CloudInitErrored,
   496  			expError: "cannot restrict cloud-init in error or enabled state",
   497  		},
   498  		{
   499  			comment:  "enable (not running)",
   500  			state:    sysconfig.CloudInitEnabled,
   501  			expError: "cannot restrict cloud-init in error or enabled state",
   502  		},
   503  		{
   504  			comment: "errored w/ force disable",
   505  			state:   sysconfig.CloudInitErrored,
   506  			sysconfOpts: &sysconfig.CloudInitRestrictOptions{
   507  				ForceDisable: true,
   508  			},
   509  			expAction:      "disable",
   510  			expDisableFile: true,
   511  		},
   512  		{
   513  			comment: "enable (not running) w/ force disable",
   514  			state:   sysconfig.CloudInitEnabled,
   515  			sysconfOpts: &sysconfig.CloudInitRestrictOptions{
   516  				ForceDisable: true,
   517  			},
   518  			expAction:      "disable",
   519  			expDisableFile: true,
   520  		},
   521  		{
   522  			comment:        "untriggered",
   523  			state:          sysconfig.CloudInitUntriggered,
   524  			expAction:      "disable",
   525  			expDisableFile: true,
   526  		},
   527  		{
   528  			comment:        "unknown status",
   529  			state:          -1,
   530  			expAction:      "disable",
   531  			expDisableFile: true,
   532  		},
   533  		{
   534  			comment:             "gce done",
   535  			state:               sysconfig.CloudInitDone,
   536  			cloudInitStatusJSON: gceCloudInitStatusJSON,
   537  			expDatasource:       "GCE",
   538  			expAction:           "restrict",
   539  			expRestrictYamlWritten: `datasource_list: [GCE]
   540  `,
   541  		},
   542  		{
   543  			comment:                "nocloud done",
   544  			state:                  sysconfig.CloudInitDone,
   545  			cloudInitStatusJSON:    multipassNoCloudCloudInitStatusJSON,
   546  			expDatasource:          "NoCloud",
   547  			expAction:              "restrict",
   548  			expRestrictYamlWritten: restrictNoCloudYaml,
   549  		},
   550  		{
   551  			comment:             "nocloud uc20 done",
   552  			state:               sysconfig.CloudInitDone,
   553  			cloudInitStatusJSON: multipassNoCloudCloudInitStatusJSON,
   554  			sysconfOpts: &sysconfig.CloudInitRestrictOptions{
   555  				DisableAfterLocalDatasourcesRun: true,
   556  			},
   557  			expDatasource:  "NoCloud",
   558  			expAction:      "disable",
   559  			expDisableFile: true,
   560  		},
   561  		{
   562  			comment:             "none uc20 done",
   563  			state:               sysconfig.CloudInitDone,
   564  			cloudInitStatusJSON: localNoneCloudInitStatusJSON,
   565  			sysconfOpts: &sysconfig.CloudInitRestrictOptions{
   566  				DisableAfterLocalDatasourcesRun: true,
   567  			},
   568  			expDatasource:  "None",
   569  			expAction:      "disable",
   570  			expDisableFile: true,
   571  		},
   572  
   573  		// the two cases for lxd and multipass are effectively the same, but as
   574  		// the largest known users of cloud-init w/ UC, we leave them as
   575  		// separate test cases for their different cloud-init status.json
   576  		// content
   577  		{
   578  			comment:                "nocloud multipass done",
   579  			state:                  sysconfig.CloudInitDone,
   580  			cloudInitStatusJSON:    multipassNoCloudCloudInitStatusJSON,
   581  			expDatasource:          "NoCloud",
   582  			expAction:              "restrict",
   583  			expRestrictYamlWritten: restrictNoCloudYaml,
   584  		},
   585  		{
   586  			comment:                "nocloud seed lxd done",
   587  			state:                  sysconfig.CloudInitDone,
   588  			cloudInitStatusJSON:    lxdNoCloudCloudInitStatusJSON,
   589  			expDatasource:          "NoCloud",
   590  			expAction:              "restrict",
   591  			expRestrictYamlWritten: restrictNoCloudYaml,
   592  		},
   593  		{
   594  			comment:             "nocloud uc20 multipass done",
   595  			state:               sysconfig.CloudInitDone,
   596  			cloudInitStatusJSON: multipassNoCloudCloudInitStatusJSON,
   597  			sysconfOpts: &sysconfig.CloudInitRestrictOptions{
   598  				DisableAfterLocalDatasourcesRun: true,
   599  			},
   600  			expDatasource:  "NoCloud",
   601  			expAction:      "disable",
   602  			expDisableFile: true,
   603  		},
   604  		{
   605  			comment:             "nocloud uc20 seed lxd done",
   606  			state:               sysconfig.CloudInitDone,
   607  			cloudInitStatusJSON: lxdNoCloudCloudInitStatusJSON,
   608  			sysconfOpts: &sysconfig.CloudInitRestrictOptions{
   609  				DisableAfterLocalDatasourcesRun: true,
   610  			},
   611  			expDatasource:  "NoCloud",
   612  			expAction:      "disable",
   613  			expDisableFile: true,
   614  		},
   615  		{
   616  			comment:        "no cloud-init in $PATH",
   617  			state:          sysconfig.CloudInitNotFound,
   618  			expAction:      "disable",
   619  			expDisableFile: true,
   620  		},
   621  	}
   622  
   623  	for _, t := range tt {
   624  		comment := Commentf("%s", t.comment)
   625  		// setup status.json
   626  		old := dirs.GlobalRootDir
   627  		dirs.SetRootDir(c.MkDir())
   628  		defer func() { dirs.SetRootDir(old) }()
   629  		statusJSONFile := filepath.Join(dirs.GlobalRootDir, "/run/cloud-init/status.json")
   630  		if t.cloudInitStatusJSON != "" {
   631  			err := os.MkdirAll(filepath.Dir(statusJSONFile), 0755)
   632  			c.Assert(err, IsNil, comment)
   633  			err = ioutil.WriteFile(statusJSONFile, []byte(t.cloudInitStatusJSON), 0644)
   634  			c.Assert(err, IsNil, comment)
   635  		}
   636  
   637  		// if we expect snapd to write a yaml config file for cloud-init, ensure
   638  		// the dir exists before hand
   639  		if t.expRestrictYamlWritten != "" {
   640  			err := os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "/etc/cloud/cloud.cfg.d"), 0755)
   641  			c.Assert(err, IsNil, comment)
   642  		}
   643  
   644  		res, err := sysconfig.RestrictCloudInit(t.state, t.sysconfOpts)
   645  		if t.expError == "" {
   646  			c.Assert(err, IsNil, comment)
   647  			c.Assert(res.DataSource, Equals, t.expDatasource, comment)
   648  			c.Assert(res.Action, Equals, t.expAction, comment)
   649  			if t.expRestrictYamlWritten != "" {
   650  				// check the snapd restrict yaml file that should have been written
   651  				c.Assert(
   652  					filepath.Join(dirs.GlobalRootDir, "/etc/cloud/cloud.cfg.d/zzzz_snapd.cfg"),
   653  					testutil.FileEquals,
   654  					t.expRestrictYamlWritten,
   655  					comment,
   656  				)
   657  			}
   658  
   659  			// if we expect the disable file to be written then check for it
   660  			// otherwise ensure it was not written accidentally
   661  			var fileCheck Checker
   662  			if t.expDisableFile {
   663  				fileCheck = testutil.FilePresent
   664  			} else {
   665  				fileCheck = testutil.FileAbsent
   666  			}
   667  
   668  			c.Assert(
   669  				filepath.Join(dirs.GlobalRootDir, "/etc/cloud/cloud-init.disabled"),
   670  				fileCheck,
   671  				comment,
   672  			)
   673  
   674  		} else {
   675  			c.Assert(err, ErrorMatches, t.expError, comment)
   676  		}
   677  	}
   678  }
   679  
   680  const maasGadgetCloudInitImplictYAML = `
   681  datasource:
   682    MAAS:
   683      foo: bar
   684  `
   685  
   686  const maasGadgetCloudInitImplictLowerCaseYAML = `
   687  datasource:
   688    maas:
   689      foo: bar
   690  `
   691  
   692  const explicitlyNoDatasourceYAML = `datasource_list: []`
   693  
   694  const explicitlyNoDatasourceButAlsoImplicitlyAnotherYAML = `
   695  datasource_list: []
   696  reporting:
   697    NoCloud:
   698      foo: bar
   699  `
   700  
   701  const explicitlyMultipleMixedCaseMentioned = `
   702  reporting:
   703    NoCloud:
   704      foo: bar
   705    maas:
   706      foo: bar
   707  datasource:
   708    MAAS:
   709      foo: bar
   710    NOCLOUD:
   711      foo: bar
   712  `
   713  
   714  func (s *sysconfigSuite) TestCloudDatasourcesInUse(c *C) {
   715  	tt := []struct {
   716  		configFileContent string
   717  		expError          string
   718  		expRes            *sysconfig.CloudDatasourcesInUseResult
   719  		comment           string
   720  	}{
   721  		{
   722  			configFileContent: `datasource_list: [MAAS]`,
   723  			expRes: &sysconfig.CloudDatasourcesInUseResult{
   724  				ExplicitlyAllowed: []string{"MAAS"},
   725  				Mentioned:         []string{"MAAS"},
   726  			},
   727  			comment: "explicitly allowed via datasource_list in upper case",
   728  		},
   729  		{
   730  			configFileContent: `datasource_list: [maas]`,
   731  			expRes: &sysconfig.CloudDatasourcesInUseResult{
   732  				ExplicitlyAllowed: []string{"MAAS"},
   733  				Mentioned:         []string{"MAAS"},
   734  			},
   735  			comment: "explicitly allowed via datasource_list in lower case",
   736  		},
   737  		{
   738  			configFileContent: `datasource_list: [mAaS]`,
   739  			expRes: &sysconfig.CloudDatasourcesInUseResult{
   740  				ExplicitlyAllowed: []string{"MAAS"},
   741  				Mentioned:         []string{"MAAS"},
   742  			},
   743  			comment: "explicitly allowed via datasource_list in random case",
   744  		},
   745  		{
   746  			configFileContent: `datasource_list: [maas, maas]`,
   747  			expRes: &sysconfig.CloudDatasourcesInUseResult{
   748  				ExplicitlyAllowed: []string{"MAAS"},
   749  				Mentioned:         []string{"MAAS"},
   750  			},
   751  			comment: "duplicated datasource in datasource_list",
   752  		},
   753  		{
   754  			configFileContent: `datasource_list: [maas, MAAS]`,
   755  			expRes: &sysconfig.CloudDatasourcesInUseResult{
   756  				ExplicitlyAllowed: []string{"MAAS"},
   757  				Mentioned:         []string{"MAAS"},
   758  			},
   759  			comment: "duplicated datasource in datasource_list with different cases",
   760  		},
   761  		{
   762  			configFileContent: `datasource_list: [maas, GCE]`,
   763  			expRes: &sysconfig.CloudDatasourcesInUseResult{
   764  				ExplicitlyAllowed: []string{"GCE", "MAAS"},
   765  				Mentioned:         []string{"GCE", "MAAS"},
   766  			},
   767  			comment: "multiple datasources in datasource list",
   768  		},
   769  		{
   770  			configFileContent: maasGadgetCloudInitImplictYAML,
   771  			expRes: &sysconfig.CloudDatasourcesInUseResult{
   772  				Mentioned: []string{"MAAS"},
   773  			},
   774  			comment: "implicitly mentioned datasource",
   775  		},
   776  		{
   777  			configFileContent: maasGadgetCloudInitImplictLowerCaseYAML,
   778  			expRes: &sysconfig.CloudDatasourcesInUseResult{
   779  				Mentioned: []string{"MAAS"},
   780  			},
   781  			comment: "implicitly mentioned datasource in lower case",
   782  		},
   783  		{
   784  			configFileContent: explicitlyNoDatasourceYAML,
   785  			expRes: &sysconfig.CloudDatasourcesInUseResult{
   786  				ExplicitlyNoneAllowed: true,
   787  			},
   788  			comment: "no datasources allowed at all",
   789  		},
   790  		{
   791  			configFileContent: explicitlyNoDatasourceButAlsoImplicitlyAnotherYAML,
   792  			expRes: &sysconfig.CloudDatasourcesInUseResult{
   793  				ExplicitlyNoneAllowed: true,
   794  				Mentioned:             []string{"NOCLOUD"},
   795  			},
   796  			comment: "explicitly no datasources allowed, but still some mentioned",
   797  		},
   798  		{
   799  			configFileContent: explicitlyMultipleMixedCaseMentioned,
   800  			expRes: &sysconfig.CloudDatasourcesInUseResult{
   801  				Mentioned: []string{"MAAS", "NOCLOUD"},
   802  			},
   803  			comment: "multiple of same datasources mentioned in different cases",
   804  		},
   805  		{
   806  			configFileContent: "i'm not yaml",
   807  			expError:          "yaml: unmarshal errors.*\n.*cannot unmarshal.*",
   808  			comment:           "invalid yaml",
   809  		},
   810  	}
   811  
   812  	for _, t := range tt {
   813  		comment := Commentf(t.comment)
   814  		configFile := filepath.Join(c.MkDir(), "cloud.conf")
   815  		err := ioutil.WriteFile(configFile, []byte(t.configFileContent), 0644)
   816  		c.Assert(err, IsNil, comment)
   817  		res, err := sysconfig.CloudDatasourcesInUse(configFile)
   818  		if t.expError != "" {
   819  			c.Assert(err, ErrorMatches, t.expError, comment)
   820  			continue
   821  		}
   822  
   823  		c.Assert(res, DeepEquals, t.expRes, comment)
   824  	}
   825  }
   826  
   827  const maasCfg1 = `#cloud-config
   828  reporting:
   829    maas:
   830      type: webhook
   831      endpoint: http://172-16-99-0--24.maas-internal:5248/MAAS/metadata/status/foo
   832      consumer_key: foothefoo
   833      token_key: foothefoothesecond
   834      token_secret: foothesecretfoo
   835  `
   836  
   837  const maasCfg2 = `datasource_list: [ MAAS ]
   838  `
   839  
   840  const maasCfg3 = `#cloud-config
   841  snappy:
   842    email: foo@foothewebsite.com
   843  `
   844  
   845  const maasCfg4 = `#cloud-config
   846  network:
   847    config:
   848    - id: enp3s0
   849      mac_address: 52:54:00:b4:9e:25
   850      mtu: 1500
   851      name: enp3s0
   852      subnets:
   853      - address: 172.16.99.7/24
   854        dns_nameservers:
   855        - 172.16.99.1
   856        dns_search:
   857        - maas
   858        type: static
   859      type: physical
   860    - address: 172.16.99.1
   861      search:
   862      - maas
   863      type: nameserver
   864    version: 1
   865  `
   866  
   867  const maasCfg5 = `#cloud-config
   868  datasource:
   869    MAAS:
   870      consumer_key: foothefoo
   871      metadata_url: http://172-16-99-0--24.maas-internal:5248/MAAS/metadata/
   872      token_key: foothefoothesecond
   873      token_secret: foothesecretfoo
   874  `
   875  
   876  func (s *sysconfigSuite) TestFilterCloudCfgFile(c *C) {
   877  	tt := []struct {
   878  		comment string
   879  		inStr   string
   880  		outStr  string
   881  		err     string
   882  	}{
   883  		{
   884  			comment: "maas reporting cloud-init config",
   885  			inStr:   maasCfg1,
   886  			outStr:  maasCfg1,
   887  		},
   888  		{
   889  			comment: "maas datasource list cloud-init config",
   890  			inStr:   maasCfg2,
   891  			outStr: `#cloud-config
   892  datasource_list:
   893  - MAAS
   894  `,
   895  		},
   896  		{
   897  			comment: "maas snappy user cloud-init config",
   898  			inStr:   maasCfg3,
   899  			// we don't support using the snappy key
   900  			outStr: "",
   901  		},
   902  		{
   903  			comment: "maas networking cloud-init config",
   904  			inStr:   maasCfg4,
   905  			outStr:  maasCfg4,
   906  		},
   907  		{
   908  			comment: "maas datasource cloud-init config",
   909  			inStr:   maasCfg5,
   910  			outStr:  maasCfg5,
   911  		},
   912  		{
   913  			comment: "unsupported datasource in datasource section cloud-init config",
   914  			inStr: `#cloud-config
   915  datasource:
   916    NoCloud:
   917      consumer_key: fooooooo
   918  `,
   919  			outStr: "",
   920  		},
   921  		{
   922  			comment: "unsupported datasource in reporting section cloud-init config",
   923  			inStr: `#cloud-config
   924  reporting:
   925    NoCloud:
   926      consumer_key: fooooooo
   927  `,
   928  			outStr: "",
   929  		},
   930  		{
   931  			comment: "unsupported datasource in datasource_list with supported one",
   932  			inStr: `#cloud-config
   933  datasource_list: [MAAS, NoCloud]
   934  `,
   935  			outStr: `#cloud-config
   936  datasource_list:
   937  - MAAS
   938  `,
   939  		},
   940  		{
   941  			comment: "unsupported datasources in multiple keys with supported ones",
   942  			inStr: `#cloud-config
   943  datasource:
   944    MAAS:
   945      consumer_key: fooooooo
   946    NoCloud:
   947      consumer_key: fooooooo
   948  
   949  reporting:
   950    MAAS:
   951      type: webhook
   952    NoCloud:
   953      type: webhook
   954  
   955  datasource_list: [MAAS, NoCloud]
   956  `,
   957  			outStr: `#cloud-config
   958  datasource:
   959    MAAS:
   960      consumer_key: fooooooo
   961  datasource_list:
   962  - MAAS
   963  reporting:
   964    MAAS:
   965      type: webhook
   966  `,
   967  		},
   968  		{
   969  			comment: "unrelated keys",
   970  			inStr: `#cloud-config
   971  datasource:
   972    MAAS:
   973      consumer_key: fooooooo
   974      foo: bar
   975  
   976  reporting:
   977    MAAS:
   978      type: webhook
   979      new_foo: new_bar
   980  
   981  extra_foo: extra_bar
   982  `,
   983  			outStr: `#cloud-config
   984  datasource:
   985    MAAS:
   986      consumer_key: fooooooo
   987  reporting:
   988    MAAS:
   989      type: webhook
   990  `,
   991  		},
   992  	}
   993  
   994  	dir := c.MkDir()
   995  	for i, t := range tt {
   996  		comment := Commentf(t.comment)
   997  		inFile := filepath.Join(dir, fmt.Sprintf("%d.cfg", i))
   998  		err := ioutil.WriteFile(inFile, []byte(t.inStr), 0755)
   999  		c.Assert(err, IsNil, comment)
  1000  
  1001  		out, err := sysconfig.FilterCloudCfgFile(inFile, []string{"MAAS"})
  1002  		if t.err != "" {
  1003  			c.Assert(err, ErrorMatches, t.err, comment)
  1004  			continue
  1005  		}
  1006  		c.Assert(err, IsNil, comment)
  1007  
  1008  		// no expected output means that everything was filtered out
  1009  		if t.outStr == "" {
  1010  			c.Assert(out, Equals, "", comment)
  1011  			continue
  1012  		}
  1013  
  1014  		// otherwise we have expected output in the file
  1015  		b, err := ioutil.ReadFile(out)
  1016  		c.Assert(err, IsNil, comment)
  1017  		c.Assert(string(b), Equals, t.outStr, comment)
  1018  	}
  1019  }