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  }