github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/overlord/devicestate/devicestate_cloudinit_test.go (about)

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