github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/environs/cloudinit/cloudinit_test.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package cloudinit_test
     5  
     6  import (
     7  	"encoding/base64"
     8  	"regexp"
     9  	"strings"
    10  
    11  	"github.com/juju/names"
    12  	jc "github.com/juju/testing/checkers"
    13  	gc "launchpad.net/gocheck"
    14  	"launchpad.net/goyaml"
    15  
    16  	"github.com/juju/juju/agent"
    17  	coreCloudinit "github.com/juju/juju/cloudinit"
    18  	"github.com/juju/juju/constraints"
    19  	"github.com/juju/juju/environs"
    20  	"github.com/juju/juju/environs/cloudinit"
    21  	"github.com/juju/juju/environs/config"
    22  	jujutesting "github.com/juju/juju/juju/testing"
    23  	"github.com/juju/juju/state"
    24  	"github.com/juju/juju/state/api"
    25  	"github.com/juju/juju/state/api/params"
    26  	"github.com/juju/juju/testing"
    27  	"github.com/juju/juju/tools"
    28  	"github.com/juju/juju/version"
    29  )
    30  
    31  // Use local suite since this file lives in the ec2 package
    32  // for testing internals.
    33  type cloudinitSuite struct {
    34  	testing.BaseSuite
    35  }
    36  
    37  var _ = gc.Suite(&cloudinitSuite{})
    38  
    39  var envConstraints = constraints.MustParse("mem=2G")
    40  
    41  var allMachineJobs = []params.MachineJob{
    42  	params.JobManageEnviron, params.JobHostUnits,
    43  }
    44  var normalMachineJobs = []params.MachineJob{
    45  	params.JobHostUnits,
    46  }
    47  
    48  type cloudinitTest struct {
    49  	cfg           cloudinit.MachineConfig
    50  	setEnvConfig  bool
    51  	expectScripts string
    52  	// inexactMatch signifies whether we allow extra lines
    53  	// in the actual scripts found. If it's true, the lines
    54  	// mentioned in expectScripts must appear in that
    55  	// order, but they can be arbitrarily interleaved with other
    56  	// script lines.
    57  	inexactMatch bool
    58  }
    59  
    60  func minimalConfig(c *gc.C) *config.Config {
    61  	cfg, err := config.New(config.NoDefaults, testing.FakeConfig())
    62  	c.Assert(err, gc.IsNil)
    63  	c.Assert(cfg, gc.NotNil)
    64  	return cfg
    65  }
    66  
    67  func must(s string, err error) string {
    68  	if err != nil {
    69  		panic(err)
    70  	}
    71  	return s
    72  }
    73  
    74  var stateServingInfo = &params.StateServingInfo{
    75  	Cert:       string(serverCert),
    76  	PrivateKey: string(serverKey),
    77  	StatePort:  37017,
    78  	APIPort:    17070,
    79  }
    80  
    81  // Each test gives a cloudinit config - we check the
    82  // output to see if it looks correct.
    83  var cloudinitTests = []cloudinitTest{
    84  	{
    85  		// precise state server
    86  		cfg: cloudinit.MachineConfig{
    87  			MachineId:        "0",
    88  			AuthorizedKeys:   "sshkey1",
    89  			AgentEnvironment: map[string]string{agent.ProviderType: "dummy"},
    90  			// precise currently needs mongo from PPA
    91  			Tools:            newSimpleTools("1.2.3-precise-amd64"),
    92  			Bootstrap:        true,
    93  			StateServingInfo: stateServingInfo,
    94  			MachineNonce:     "FAKE_NONCE",
    95  			StateInfo: &state.Info{
    96  				Password: "arble",
    97  				CACert:   "CA CERT\n" + testing.CACert,
    98  			},
    99  			APIInfo: &api.Info{
   100  				Password: "bletch",
   101  				CACert:   "CA CERT\n" + testing.CACert,
   102  			},
   103  			Constraints:             envConstraints,
   104  			DataDir:                 environs.DataDir,
   105  			LogDir:                  agent.DefaultLogDir,
   106  			Jobs:                    allMachineJobs,
   107  			CloudInitOutputLog:      environs.CloudInitOutputLog,
   108  			InstanceId:              "i-bootstrap",
   109  			SystemPrivateSSHKey:     "private rsa key",
   110  			MachineAgentServiceName: "jujud-machine-0",
   111  		},
   112  		setEnvConfig: true,
   113  		expectScripts: `
   114  set -xe
   115  install -D -m 644 /dev/null '/var/lib/juju/nonce.txt'
   116  printf '%s\\n' 'FAKE_NONCE' > '/var/lib/juju/nonce.txt'
   117  test -e /proc/self/fd/9 \|\| exec 9>&2
   118  \(\[ ! -e /home/ubuntu/.profile \] \|\| grep -q '.juju-proxy' /home/ubuntu/.profile\) \|\| printf .* >> /home/ubuntu/.profile
   119  mkdir -p /var/lib/juju/locks
   120  \[ -e /home/ubuntu \] && chown ubuntu:ubuntu /var/lib/juju/locks
   121  mkdir -p /var/log/juju
   122  chown syslog:adm /var/log/juju
   123  echo 'Fetching tools.*
   124  bin='/var/lib/juju/tools/1\.2\.3-precise-amd64'
   125  mkdir -p \$bin
   126  curl -sSfw 'tools from %{url_effective} downloaded: HTTP %{http_code}; time %{time_total}s; size %{size_download} bytes; speed %{speed_download} bytes/s ' -o \$bin/tools\.tar\.gz 'http://foo\.com/tools/releases/juju1\.2\.3-precise-amd64\.tgz'
   127  sha256sum \$bin/tools\.tar\.gz > \$bin/juju1\.2\.3-precise-amd64\.sha256
   128  grep '1234' \$bin/juju1\.2\.3-precise-amd64.sha256 \|\| \(echo "Tools checksum mismatch"; exit 1\)
   129  tar zxf \$bin/tools.tar.gz -C \$bin
   130  rm \$bin/tools\.tar\.gz && rm \$bin/juju1\.2\.3-precise-amd64\.sha256
   131  printf %s '{"version":"1\.2\.3-precise-amd64","url":"http://foo\.com/tools/releases/juju1\.2\.3-precise-amd64\.tgz","sha256":"1234","size":10}' > \$bin/downloaded-tools\.txt
   132  mkdir -p '/var/lib/juju/agents/machine-0'
   133  install -m 600 /dev/null '/var/lib/juju/agents/machine-0/agent\.conf'
   134  printf '%s\\n' '.*' > '/var/lib/juju/agents/machine-0/agent\.conf'
   135  install -D -m 644 /dev/null '/etc/apt/preferences\.d/50-cloud-tools'
   136  printf '%s\\n' '.*' > '/etc/apt/preferences\.d/50-cloud-tools'
   137  echo 'Bootstrapping Juju machine agent'.*
   138  /var/lib/juju/tools/1\.2\.3-precise-amd64/jujud bootstrap-state --data-dir '/var/lib/juju' --env-config '[^']*' --instance-id 'i-bootstrap' --constraints 'mem=2048M' --debug
   139  ln -s 1\.2\.3-precise-amd64 '/var/lib/juju/tools/machine-0'
   140  echo 'Starting Juju machine agent \(jujud-machine-0\)'.*
   141  cat >> /etc/init/jujud-machine-0\.conf << 'EOF'\\ndescription "juju machine-0 agent"\\nauthor "Juju Team <juju@lists\.ubuntu\.com>"\\nstart on runlevel \[2345\]\\nstop on runlevel \[!2345\]\\nrespawn\\nnormal exit 0\\n\\nlimit nofile 20000 20000\\n\\nexec /var/lib/juju/tools/machine-0/jujud machine --data-dir '/var/lib/juju' --machine-id 0 --debug >> /var/log/juju/machine-0\.log 2>&1\\nEOF\\n
   142  start jujud-machine-0
   143  `,
   144  	}, {
   145  		// raring state server - we just test the raring-specific parts of the output.
   146  		cfg: cloudinit.MachineConfig{
   147  			MachineId:        "0",
   148  			AuthorizedKeys:   "sshkey1",
   149  			AgentEnvironment: map[string]string{agent.ProviderType: "dummy"},
   150  			// raring provides mongo in the archive
   151  			Tools:            newSimpleTools("1.2.3-raring-amd64"),
   152  			Bootstrap:        true,
   153  			StateServingInfo: stateServingInfo,
   154  			MachineNonce:     "FAKE_NONCE",
   155  			StateInfo: &state.Info{
   156  				Password: "arble",
   157  				CACert:   "CA CERT\n" + testing.CACert,
   158  			},
   159  			APIInfo: &api.Info{
   160  				Password: "bletch",
   161  				CACert:   "CA CERT\n" + testing.CACert,
   162  			},
   163  			Constraints:             envConstraints,
   164  			DataDir:                 environs.DataDir,
   165  			LogDir:                  agent.DefaultLogDir,
   166  			Jobs:                    allMachineJobs,
   167  			CloudInitOutputLog:      environs.CloudInitOutputLog,
   168  			InstanceId:              "i-bootstrap",
   169  			SystemPrivateSSHKey:     "private rsa key",
   170  			MachineAgentServiceName: "jujud-machine-0",
   171  		},
   172  		setEnvConfig: true,
   173  		inexactMatch: true,
   174  		expectScripts: `
   175  bin='/var/lib/juju/tools/1\.2\.3-raring-amd64'
   176  curl -sSfw 'tools from %{url_effective} downloaded: HTTP %{http_code}; time %{time_total}s; size %{size_download} bytes; speed %{speed_download} bytes/s ' -o \$bin/tools\.tar\.gz 'http://foo\.com/tools/releases/juju1\.2\.3-raring-amd64\.tgz'
   177  sha256sum \$bin/tools\.tar\.gz > \$bin/juju1\.2\.3-raring-amd64\.sha256
   178  grep '1234' \$bin/juju1\.2\.3-raring-amd64.sha256 \|\| \(echo "Tools checksum mismatch"; exit 1\)
   179  rm \$bin/tools\.tar\.gz && rm \$bin/juju1\.2\.3-raring-amd64\.sha256
   180  printf %s '{"version":"1\.2\.3-raring-amd64","url":"http://foo\.com/tools/releases/juju1\.2\.3-raring-amd64\.tgz","sha256":"1234","size":10}' > \$bin/downloaded-tools\.txt
   181  /var/lib/juju/tools/1\.2\.3-raring-amd64/jujud bootstrap-state --data-dir '/var/lib/juju' --env-config '[^']*' --instance-id 'i-bootstrap' --constraints 'mem=2048M' --debug
   182  ln -s 1\.2\.3-raring-amd64 '/var/lib/juju/tools/machine-0'
   183  `,
   184  	}, {
   185  		// non state server.
   186  		cfg: cloudinit.MachineConfig{
   187  			MachineId:          "99",
   188  			AuthorizedKeys:     "sshkey1",
   189  			AgentEnvironment:   map[string]string{agent.ProviderType: "dummy"},
   190  			DataDir:            environs.DataDir,
   191  			LogDir:             agent.DefaultLogDir,
   192  			Jobs:               normalMachineJobs,
   193  			CloudInitOutputLog: environs.CloudInitOutputLog,
   194  			Bootstrap:          false,
   195  			Tools:              newSimpleTools("1.2.3-linux-amd64"),
   196  			MachineNonce:       "FAKE_NONCE",
   197  			StateInfo: &state.Info{
   198  				Addrs:    []string{"state-addr.testing.invalid:12345"},
   199  				Tag:      "machine-99",
   200  				Password: "arble",
   201  				CACert:   "CA CERT\n" + testing.CACert,
   202  			},
   203  			APIInfo: &api.Info{
   204  				Addrs:    []string{"state-addr.testing.invalid:54321"},
   205  				Tag:      "machine-99",
   206  				Password: "bletch",
   207  				CACert:   "CA CERT\n" + testing.CACert,
   208  			},
   209  			MachineAgentServiceName: "jujud-machine-99",
   210  		},
   211  		expectScripts: `
   212  set -xe
   213  install -D -m 644 /dev/null '/var/lib/juju/nonce.txt'
   214  printf '%s\\n' 'FAKE_NONCE' > '/var/lib/juju/nonce.txt'
   215  test -e /proc/self/fd/9 \|\| exec 9>&2
   216  \(\[ ! -e /home/ubuntu/\.profile \] \|\| grep -q '.juju-proxy' /home/ubuntu/.profile\) \|\| printf .* >> /home/ubuntu/.profile
   217  mkdir -p /var/lib/juju/locks
   218  \[ -e /home/ubuntu \] && chown ubuntu:ubuntu /var/lib/juju/locks
   219  mkdir -p /var/log/juju
   220  chown syslog:adm /var/log/juju
   221  echo 'Fetching tools.*
   222  bin='/var/lib/juju/tools/1\.2\.3-linux-amd64'
   223  mkdir -p \$bin
   224  curl -sSfw 'tools from %{url_effective} downloaded: HTTP %{http_code}; time %{time_total}s; size %{size_download} bytes; speed %{speed_download} bytes/s ' -o \$bin/tools\.tar\.gz 'http://foo\.com/tools/releases/juju1\.2\.3-linux-amd64\.tgz'
   225  sha256sum \$bin/tools\.tar\.gz > \$bin/juju1\.2\.3-linux-amd64\.sha256
   226  grep '1234' \$bin/juju1\.2\.3-linux-amd64.sha256 \|\| \(echo "Tools checksum mismatch"; exit 1\)
   227  tar zxf \$bin/tools.tar.gz -C \$bin
   228  rm \$bin/tools\.tar\.gz && rm \$bin/juju1\.2\.3-linux-amd64\.sha256
   229  printf %s '{"version":"1\.2\.3-linux-amd64","url":"http://foo\.com/tools/releases/juju1\.2\.3-linux-amd64\.tgz","sha256":"1234","size":10}' > \$bin/downloaded-tools\.txt
   230  mkdir -p '/var/lib/juju/agents/machine-99'
   231  install -m 600 /dev/null '/var/lib/juju/agents/machine-99/agent\.conf'
   232  printf '%s\\n' '.*' > '/var/lib/juju/agents/machine-99/agent\.conf'
   233  ln -s 1\.2\.3-linux-amd64 '/var/lib/juju/tools/machine-99'
   234  echo 'Starting Juju machine agent \(jujud-machine-99\)'.*
   235  cat >> /etc/init/jujud-machine-99\.conf << 'EOF'\\ndescription "juju machine-99 agent"\\nauthor "Juju Team <juju@lists\.ubuntu\.com>"\\nstart on runlevel \[2345\]\\nstop on runlevel \[!2345\]\\nrespawn\\nnormal exit 0\\n\\nlimit nofile 20000 20000\\n\\nexec /var/lib/juju/tools/machine-99/jujud machine --data-dir '/var/lib/juju' --machine-id 99 --debug >> /var/log/juju/machine-99\.log 2>&1\\nEOF\\n
   236  start jujud-machine-99
   237  `,
   238  	}, {
   239  		// check that it works ok with compound machine ids.
   240  		cfg: cloudinit.MachineConfig{
   241  			MachineId:            "2/lxc/1",
   242  			MachineContainerType: "lxc",
   243  			AuthorizedKeys:       "sshkey1",
   244  			AgentEnvironment:     map[string]string{agent.ProviderType: "dummy"},
   245  			DataDir:              environs.DataDir,
   246  			LogDir:               agent.DefaultLogDir,
   247  			Jobs:                 normalMachineJobs,
   248  			CloudInitOutputLog:   environs.CloudInitOutputLog,
   249  			Bootstrap:            false,
   250  			Tools:                newSimpleTools("1.2.3-linux-amd64"),
   251  			MachineNonce:         "FAKE_NONCE",
   252  			StateInfo: &state.Info{
   253  				Addrs:    []string{"state-addr.testing.invalid:12345"},
   254  				Tag:      "machine-2-lxc-1",
   255  				Password: "arble",
   256  				CACert:   "CA CERT\n" + testing.CACert,
   257  			},
   258  			APIInfo: &api.Info{
   259  				Addrs:    []string{"state-addr.testing.invalid:54321"},
   260  				Tag:      "machine-2-lxc-1",
   261  				Password: "bletch",
   262  				CACert:   "CA CERT\n" + testing.CACert,
   263  			},
   264  			MachineAgentServiceName: "jujud-machine-2-lxc-1",
   265  		},
   266  		inexactMatch: true,
   267  		expectScripts: `
   268  mkdir -p '/var/lib/juju/agents/machine-2-lxc-1'
   269  install -m 600 /dev/null '/var/lib/juju/agents/machine-2-lxc-1/agent\.conf'
   270  printf '%s\\n' '.*' > '/var/lib/juju/agents/machine-2-lxc-1/agent\.conf'
   271  ln -s 1\.2\.3-linux-amd64 '/var/lib/juju/tools/machine-2-lxc-1'
   272  cat >> /etc/init/jujud-machine-2-lxc-1\.conf << 'EOF'\\ndescription "juju machine-2-lxc-1 agent"\\nauthor "Juju Team <juju@lists\.ubuntu\.com>"\\nstart on runlevel \[2345\]\\nstop on runlevel \[!2345\]\\nrespawn\\nnormal exit 0\\n\\nlimit nofile 20000 20000\\n\\nexec /var/lib/juju/tools/machine-2-lxc-1/jujud machine --data-dir '/var/lib/juju' --machine-id 2/lxc/1 --debug >> /var/log/juju/machine-2-lxc-1\.log 2>&1\\nEOF\\n
   273  start jujud-machine-2-lxc-1
   274  `,
   275  	}, {
   276  		// hostname verification disabled.
   277  		cfg: cloudinit.MachineConfig{
   278  			MachineId:          "99",
   279  			AuthorizedKeys:     "sshkey1",
   280  			AgentEnvironment:   map[string]string{agent.ProviderType: "dummy"},
   281  			DataDir:            environs.DataDir,
   282  			LogDir:             agent.DefaultLogDir,
   283  			Jobs:               normalMachineJobs,
   284  			CloudInitOutputLog: environs.CloudInitOutputLog,
   285  			Bootstrap:          false,
   286  			Tools:              newSimpleTools("1.2.3-linux-amd64"),
   287  			MachineNonce:       "FAKE_NONCE",
   288  			StateInfo: &state.Info{
   289  				Addrs:    []string{"state-addr.testing.invalid:12345"},
   290  				Tag:      "machine-99",
   291  				Password: "arble",
   292  				CACert:   "CA CERT\n" + testing.CACert,
   293  			},
   294  			APIInfo: &api.Info{
   295  				Addrs:    []string{"state-addr.testing.invalid:54321"},
   296  				Tag:      "machine-99",
   297  				Password: "bletch",
   298  				CACert:   "CA CERT\n" + testing.CACert,
   299  			},
   300  			DisableSSLHostnameVerification: true,
   301  			MachineAgentServiceName:        "jujud-machine-99",
   302  		},
   303  		inexactMatch: true,
   304  		expectScripts: `
   305  curl -sSfw 'tools from %{url_effective} downloaded: HTTP %{http_code}; time %{time_total}s; size %{size_download} bytes; speed %{speed_download} bytes/s ' --insecure -o \$bin/tools\.tar\.gz 'http://foo\.com/tools/releases/juju1\.2\.3-linux-amd64\.tgz'
   306  `,
   307  	}, {
   308  		// empty contraints.
   309  		cfg: cloudinit.MachineConfig{
   310  			MachineId:        "0",
   311  			AuthorizedKeys:   "sshkey1",
   312  			AgentEnvironment: map[string]string{agent.ProviderType: "dummy"},
   313  			// precise currently needs mongo from PPA
   314  			Tools:            newSimpleTools("1.2.3-precise-amd64"),
   315  			Bootstrap:        true,
   316  			StateServingInfo: stateServingInfo,
   317  			MachineNonce:     "FAKE_NONCE",
   318  			StateInfo: &state.Info{
   319  				Password: "arble",
   320  				CACert:   "CA CERT\n" + testing.CACert,
   321  			},
   322  			APIInfo: &api.Info{
   323  				Password: "bletch",
   324  				CACert:   "CA CERT\n" + testing.CACert,
   325  			},
   326  			DataDir:                 environs.DataDir,
   327  			LogDir:                  agent.DefaultLogDir,
   328  			Jobs:                    allMachineJobs,
   329  			CloudInitOutputLog:      environs.CloudInitOutputLog,
   330  			InstanceId:              "i-bootstrap",
   331  			SystemPrivateSSHKey:     "private rsa key",
   332  			MachineAgentServiceName: "jujud-machine-0",
   333  		},
   334  		setEnvConfig: true,
   335  		inexactMatch: true,
   336  		expectScripts: `
   337  /var/lib/juju/tools/1\.2\.3-precise-amd64/jujud bootstrap-state --data-dir '/var/lib/juju' --env-config '[^']*' --instance-id 'i-bootstrap' --debug
   338  `,
   339  	},
   340  }
   341  
   342  func newSimpleTools(vers string) *tools.Tools {
   343  	return &tools.Tools{
   344  		URL:     "http://foo.com/tools/releases/juju" + vers + ".tgz",
   345  		Version: version.MustParseBinary(vers),
   346  		Size:    10,
   347  		SHA256:  "1234",
   348  	}
   349  }
   350  
   351  func newFileTools(vers, path string) *tools.Tools {
   352  	tools := newSimpleTools(vers)
   353  	tools.URL = "file://" + path
   354  	return tools
   355  }
   356  
   357  func getAgentConfig(c *gc.C, tag string, scripts []string) (cfg string) {
   358  	re := regexp.MustCompile(`printf '%s\\n' '((\n|.)+)' > .*agents/` + regexp.QuoteMeta(tag) + `/agent\.conf`)
   359  	found := false
   360  	for _, s := range scripts {
   361  		m := re.FindStringSubmatch(s)
   362  		if m == nil {
   363  			continue
   364  		}
   365  		cfg = m[1]
   366  		found = true
   367  	}
   368  	c.Assert(found, gc.Equals, true)
   369  	return cfg
   370  }
   371  
   372  // check that any --env-config $base64 is valid and matches t.cfg.Config
   373  func checkEnvConfig(c *gc.C, cfg *config.Config, x map[interface{}]interface{}, scripts []string) {
   374  	re := regexp.MustCompile(`--env-config '([^']+)'`)
   375  	found := false
   376  	for _, s := range scripts {
   377  		m := re.FindStringSubmatch(s)
   378  		if m == nil {
   379  			continue
   380  		}
   381  		found = true
   382  		buf, err := base64.StdEncoding.DecodeString(m[1])
   383  		c.Assert(err, gc.IsNil)
   384  		var actual map[string]interface{}
   385  		err = goyaml.Unmarshal(buf, &actual)
   386  		c.Assert(err, gc.IsNil)
   387  		c.Assert(cfg.AllAttrs(), jc.DeepEquals, actual)
   388  	}
   389  	c.Assert(found, gc.Equals, true)
   390  }
   391  
   392  // TestCloudInit checks that the output from the various tests
   393  // in cloudinitTests is well formed.
   394  func (*cloudinitSuite) TestCloudInit(c *gc.C) {
   395  	for i, test := range cloudinitTests {
   396  		c.Logf("test %d", i)
   397  		if test.setEnvConfig {
   398  			test.cfg.Config = minimalConfig(c)
   399  		}
   400  		ci := coreCloudinit.New()
   401  		err := cloudinit.Configure(&test.cfg, ci)
   402  		c.Assert(err, gc.IsNil)
   403  		c.Check(ci, gc.NotNil)
   404  		// render the cloudinit config to bytes, and then
   405  		// back to a map so we can introspect it without
   406  		// worrying about internal details of the cloudinit
   407  		// package.
   408  		data, err := ci.Render()
   409  		c.Assert(err, gc.IsNil)
   410  
   411  		x := make(map[interface{}]interface{})
   412  		err = goyaml.Unmarshal(data, &x)
   413  		c.Assert(err, gc.IsNil)
   414  
   415  		c.Check(x["apt_upgrade"], gc.Equals, true)
   416  		c.Check(x["apt_update"], gc.Equals, true)
   417  
   418  		scripts := getScripts(x)
   419  		assertScriptMatch(c, scripts, test.expectScripts, !test.inexactMatch)
   420  		if test.cfg.Config != nil {
   421  			checkEnvConfig(c, test.cfg.Config, x, scripts)
   422  		}
   423  		checkPackage(c, x, "git", true)
   424  		tag := names.MachineTag(test.cfg.MachineId)
   425  		acfg := getAgentConfig(c, tag, scripts)
   426  		c.Assert(acfg, jc.Contains, "AGENT_SERVICE_NAME: jujud-"+tag)
   427  		source := "deb http://ubuntu-cloud.archive.canonical.com/ubuntu precise-updates/cloud-tools main"
   428  		needCloudArchive := test.cfg.Tools.Version.Series == "precise"
   429  		checkAptSource(c, x, source, cloudinit.CanonicalCloudArchiveSigningKey, needCloudArchive)
   430  	}
   431  }
   432  
   433  func (*cloudinitSuite) TestCloudInitConfigure(c *gc.C) {
   434  	for i, test := range cloudinitTests {
   435  		test.cfg.Config = minimalConfig(c)
   436  		c.Logf("test %d (Configure)", i)
   437  		cloudcfg := coreCloudinit.New()
   438  		err := cloudinit.Configure(&test.cfg, cloudcfg)
   439  		c.Assert(err, gc.IsNil)
   440  	}
   441  }
   442  
   443  func (*cloudinitSuite) TestCloudInitConfigureUsesGivenConfig(c *gc.C) {
   444  	// Create a simple cloudinit config with a 'runcmd' statement.
   445  	cloudcfg := coreCloudinit.New()
   446  	script := "test script"
   447  	cloudcfg.AddRunCmd(script)
   448  	cloudinitTests[0].cfg.Config = minimalConfig(c)
   449  	err := cloudinit.Configure(&cloudinitTests[0].cfg, cloudcfg)
   450  	c.Assert(err, gc.IsNil)
   451  	data, err := cloudcfg.Render()
   452  	c.Assert(err, gc.IsNil)
   453  
   454  	ciContent := make(map[interface{}]interface{})
   455  	err = goyaml.Unmarshal(data, &ciContent)
   456  	c.Assert(err, gc.IsNil)
   457  	// The 'runcmd' statement is at the beginning of the list
   458  	// of 'runcmd' statements.
   459  	runCmd := ciContent["runcmd"].([]interface{})
   460  	c.Check(runCmd[0], gc.Equals, script)
   461  }
   462  
   463  func getScripts(x map[interface{}]interface{}) []string {
   464  	var scripts []string
   465  	if bootcmds, ok := x["bootcmd"]; ok {
   466  		for _, s := range bootcmds.([]interface{}) {
   467  			scripts = append(scripts, s.(string))
   468  		}
   469  	}
   470  	for _, s := range x["runcmd"].([]interface{}) {
   471  		scripts = append(scripts, s.(string))
   472  	}
   473  	return scripts
   474  }
   475  
   476  type line struct {
   477  	index int
   478  	line  string
   479  }
   480  
   481  func assertScriptMatch(c *gc.C, got []string, expect string, exact bool) {
   482  	for _, s := range got {
   483  		c.Logf("script: %s", regexp.QuoteMeta(strings.Replace(s, "\n", "\\n", -1)))
   484  	}
   485  	var pats []line
   486  	for i, pat := range strings.Split(strings.Trim(expect, "\n"), "\n") {
   487  		pats = append(pats, line{
   488  			index: i,
   489  			line:  pat,
   490  		})
   491  	}
   492  	var scripts []line
   493  	for i := range got {
   494  		scripts = append(scripts, line{
   495  			index: i,
   496  			line:  strings.Replace(got[i], "\n", "\\n", -1), // make .* work
   497  		})
   498  	}
   499  	for {
   500  		switch {
   501  		case len(pats) == 0 && len(scripts) == 0:
   502  			return
   503  		case len(pats) == 0:
   504  			if exact {
   505  				c.Fatalf("too many scripts found (got %q at line %d)", scripts[0].line, scripts[0].index)
   506  			}
   507  			return
   508  		case len(scripts) == 0:
   509  			if exact {
   510  				c.Fatalf("too few scripts found (expected %q at line %d)", pats[0].line, pats[0].index)
   511  			}
   512  			c.Fatalf("could not find match for %q", pats[0].line)
   513  		default:
   514  			ok, err := regexp.MatchString(pats[0].line, scripts[0].line)
   515  			c.Assert(err, gc.IsNil)
   516  			if ok {
   517  				pats = pats[1:]
   518  				scripts = scripts[1:]
   519  			} else if exact {
   520  				c.Assert(scripts[0].line, gc.Matches, pats[0].line, gc.Commentf("line %d", scripts[0].index))
   521  				panic("unreachable")
   522  			} else {
   523  				scripts = scripts[1:]
   524  			}
   525  		}
   526  	}
   527  }
   528  
   529  // checkPackage checks that the cloudinit will or won't install the given
   530  // package, depending on the value of match.
   531  func checkPackage(c *gc.C, x map[interface{}]interface{}, pkg string, match bool) {
   532  	pkgs0 := x["packages"]
   533  	if pkgs0 == nil {
   534  		if match {
   535  			c.Errorf("cloudinit has no entry for packages")
   536  		}
   537  		return
   538  	}
   539  
   540  	pkgs := pkgs0.([]interface{})
   541  
   542  	found := false
   543  	for _, p0 := range pkgs {
   544  		p := p0.(string)
   545  		hasTargetRelease := strings.Contains(p, "--target-release")
   546  		hasQuotedPkg := strings.Contains(p, "'"+pkg+"'")
   547  		if p == pkg || (hasTargetRelease && hasQuotedPkg) {
   548  			found = true
   549  		}
   550  	}
   551  	switch {
   552  	case match && !found:
   553  		c.Errorf("package %q not found in %v", pkg, pkgs)
   554  	case !match && found:
   555  		c.Errorf("%q found but not expected in %v", pkg, pkgs)
   556  	}
   557  }
   558  
   559  // checkAptSource checks that the cloudinit will or won't install the given
   560  // source, depending on the value of match.
   561  func checkAptSource(c *gc.C, x map[interface{}]interface{}, source, key string, match bool) {
   562  	sources0 := x["apt_sources"]
   563  	if sources0 == nil {
   564  		if match {
   565  			c.Errorf("cloudinit has no entry for apt_sources")
   566  		}
   567  		return
   568  	}
   569  
   570  	sources := sources0.([]interface{})
   571  
   572  	found := false
   573  	for _, s0 := range sources {
   574  		s := s0.(map[interface{}]interface{})
   575  		if s["source"] == source && s["key"] == key {
   576  			found = true
   577  		}
   578  	}
   579  	switch {
   580  	case match && !found:
   581  		c.Errorf("source %q not found in %v", source, sources)
   582  	case !match && found:
   583  		c.Errorf("%q found but not expected in %v", source, sources)
   584  	}
   585  }
   586  
   587  // When mutate is called on a known-good MachineConfig,
   588  // there should be an error complaining about the missing
   589  // field named by the adjacent err.
   590  var verifyTests = []struct {
   591  	err    string
   592  	mutate func(*cloudinit.MachineConfig)
   593  }{
   594  	{"invalid machine id", func(cfg *cloudinit.MachineConfig) {
   595  		cfg.MachineId = "-1"
   596  	}},
   597  	{"missing environment configuration", func(cfg *cloudinit.MachineConfig) {
   598  		cfg.Config = nil
   599  	}},
   600  	{"missing state info", func(cfg *cloudinit.MachineConfig) {
   601  		cfg.StateInfo = nil
   602  	}},
   603  	{"missing API info", func(cfg *cloudinit.MachineConfig) {
   604  		cfg.APIInfo = nil
   605  	}},
   606  	{"missing state hosts", func(cfg *cloudinit.MachineConfig) {
   607  		cfg.Bootstrap = false
   608  		cfg.StateInfo = &state.Info{
   609  			Tag:    "machine-99",
   610  			CACert: testing.CACert,
   611  		}
   612  		cfg.APIInfo = &api.Info{
   613  			Addrs:  []string{"foo:35"},
   614  			Tag:    "machine-99",
   615  			CACert: testing.CACert,
   616  		}
   617  	}},
   618  	{"missing API hosts", func(cfg *cloudinit.MachineConfig) {
   619  		cfg.Bootstrap = false
   620  		cfg.StateInfo = &state.Info{
   621  			Addrs:  []string{"foo:35"},
   622  			Tag:    "machine-99",
   623  			CACert: testing.CACert,
   624  		}
   625  		cfg.APIInfo = &api.Info{
   626  			Tag:    "machine-99",
   627  			CACert: testing.CACert,
   628  		}
   629  	}},
   630  	{"missing CA certificate", func(cfg *cloudinit.MachineConfig) {
   631  		cfg.StateInfo = &state.Info{Addrs: []string{"host:98765"}}
   632  	}},
   633  	{"missing CA certificate", func(cfg *cloudinit.MachineConfig) {
   634  		cfg.Bootstrap = false
   635  		cfg.StateInfo = &state.Info{
   636  			Tag:   "machine-99",
   637  			Addrs: []string{"host:98765"},
   638  		}
   639  	}},
   640  	{"missing state server certificate", func(cfg *cloudinit.MachineConfig) {
   641  		info := *cfg.StateServingInfo
   642  		info.Cert = ""
   643  		cfg.StateServingInfo = &info
   644  	}},
   645  	{"missing state server private key", func(cfg *cloudinit.MachineConfig) {
   646  		info := *cfg.StateServingInfo
   647  		info.PrivateKey = ""
   648  		cfg.StateServingInfo = &info
   649  	}},
   650  	{"missing state port", func(cfg *cloudinit.MachineConfig) {
   651  		info := *cfg.StateServingInfo
   652  		info.StatePort = 0
   653  		cfg.StateServingInfo = &info
   654  	}},
   655  	{"missing API port", func(cfg *cloudinit.MachineConfig) {
   656  		info := *cfg.StateServingInfo
   657  		info.APIPort = 0
   658  		cfg.StateServingInfo = &info
   659  	}},
   660  	{"missing var directory", func(cfg *cloudinit.MachineConfig) {
   661  		cfg.DataDir = ""
   662  	}},
   663  	{"missing log directory", func(cfg *cloudinit.MachineConfig) {
   664  		cfg.LogDir = ""
   665  	}},
   666  	{"missing cloud-init output log path", func(cfg *cloudinit.MachineConfig) {
   667  		cfg.CloudInitOutputLog = ""
   668  	}},
   669  	{"missing tools", func(cfg *cloudinit.MachineConfig) {
   670  		cfg.Tools = nil
   671  	}},
   672  	{"missing tools URL", func(cfg *cloudinit.MachineConfig) {
   673  		cfg.Tools = &tools.Tools{}
   674  	}},
   675  	{"entity tag must match started machine", func(cfg *cloudinit.MachineConfig) {
   676  		cfg.Bootstrap = false
   677  		info := *cfg.StateInfo
   678  		info.Tag = "machine-0"
   679  		cfg.StateInfo = &info
   680  	}},
   681  	{"entity tag must match started machine", func(cfg *cloudinit.MachineConfig) {
   682  		cfg.Bootstrap = false
   683  		info := *cfg.StateInfo
   684  		info.Tag = ""
   685  		cfg.StateInfo = &info
   686  	}},
   687  	{"entity tag must match started machine", func(cfg *cloudinit.MachineConfig) {
   688  		cfg.Bootstrap = false
   689  		info := *cfg.APIInfo
   690  		info.Tag = "machine-0"
   691  		cfg.APIInfo = &info
   692  	}},
   693  	{"entity tag must match started machine", func(cfg *cloudinit.MachineConfig) {
   694  		cfg.Bootstrap = false
   695  		info := *cfg.APIInfo
   696  		info.Tag = ""
   697  		cfg.APIInfo = &info
   698  	}},
   699  	{"entity tag must be blank when starting a state server", func(cfg *cloudinit.MachineConfig) {
   700  		info := *cfg.StateInfo
   701  		info.Tag = "machine-0"
   702  		cfg.StateInfo = &info
   703  	}},
   704  	{"entity tag must be blank when starting a state server", func(cfg *cloudinit.MachineConfig) {
   705  		info := *cfg.APIInfo
   706  		info.Tag = "machine-0"
   707  		cfg.APIInfo = &info
   708  	}},
   709  	{"missing machine nonce", func(cfg *cloudinit.MachineConfig) {
   710  		cfg.MachineNonce = ""
   711  	}},
   712  	{"missing machine agent service name", func(cfg *cloudinit.MachineConfig) {
   713  		cfg.MachineAgentServiceName = ""
   714  	}},
   715  	{"missing instance-id", func(cfg *cloudinit.MachineConfig) {
   716  		cfg.InstanceId = ""
   717  	}},
   718  	{"state serving info unexpectedly present", func(cfg *cloudinit.MachineConfig) {
   719  		cfg.Bootstrap = false
   720  		apiInfo := *cfg.APIInfo
   721  		apiInfo.Tag = "machine-99"
   722  		cfg.APIInfo = &apiInfo
   723  		stateInfo := *cfg.StateInfo
   724  		stateInfo.Tag = "machine-99"
   725  		cfg.StateInfo = &stateInfo
   726  	}},
   727  }
   728  
   729  // TestCloudInitVerify checks that required fields are appropriately
   730  // checked for by NewCloudInit.
   731  func (*cloudinitSuite) TestCloudInitVerify(c *gc.C) {
   732  	cfg := &cloudinit.MachineConfig{
   733  		Bootstrap:        true,
   734  		StateServingInfo: stateServingInfo,
   735  		MachineId:        "99",
   736  		Tools:            newSimpleTools("9.9.9-linux-arble"),
   737  		AuthorizedKeys:   "sshkey1",
   738  		AgentEnvironment: map[string]string{agent.ProviderType: "dummy"},
   739  		StateInfo: &state.Info{
   740  			Addrs:    []string{"host:98765"},
   741  			CACert:   testing.CACert,
   742  			Password: "password",
   743  		},
   744  		APIInfo: &api.Info{
   745  			Addrs:  []string{"host:9999"},
   746  			CACert: testing.CACert,
   747  		},
   748  		Config:                  minimalConfig(c),
   749  		DataDir:                 environs.DataDir,
   750  		LogDir:                  agent.DefaultLogDir,
   751  		Jobs:                    normalMachineJobs,
   752  		CloudInitOutputLog:      environs.CloudInitOutputLog,
   753  		InstanceId:              "i-bootstrap",
   754  		MachineNonce:            "FAKE_NONCE",
   755  		SystemPrivateSSHKey:     "private rsa key",
   756  		MachineAgentServiceName: "jujud-machine-99",
   757  	}
   758  	// check that the base configuration does not give an error
   759  	ci := coreCloudinit.New()
   760  
   761  	for i, test := range verifyTests {
   762  		// check that the base configuration does not give an error
   763  		// and that a previous test hasn't mutated it accidentially.
   764  		err := cloudinit.Configure(cfg, ci)
   765  		c.Assert(err, gc.IsNil)
   766  
   767  		c.Logf("test %d. %s", i, test.err)
   768  
   769  		cfg1 := *cfg
   770  		test.mutate(&cfg1)
   771  
   772  		err = cloudinit.Configure(&cfg1, ci)
   773  		c.Assert(err, gc.ErrorMatches, "invalid machine configuration: "+test.err)
   774  
   775  	}
   776  }
   777  
   778  func (*cloudinitSuite) createMachineConfig(c *gc.C, environConfig *config.Config) *cloudinit.MachineConfig {
   779  	machineId := "42"
   780  	machineNonce := "fake-nonce"
   781  	stateInfo := jujutesting.FakeStateInfo(machineId)
   782  	apiInfo := jujutesting.FakeAPIInfo(machineId)
   783  	machineConfig := environs.NewMachineConfig(machineId, machineNonce, nil, stateInfo, apiInfo)
   784  	machineConfig.Tools = &tools.Tools{
   785  		Version: version.MustParseBinary("2.3.4-foo-bar"),
   786  		URL:     "http://tools.testing.invalid/2.3.4-foo-bar.tgz",
   787  	}
   788  	err := environs.FinishMachineConfig(machineConfig, environConfig, constraints.Value{})
   789  	c.Assert(err, gc.IsNil)
   790  	return machineConfig
   791  }
   792  
   793  func (s *cloudinitSuite) TestAptProxyNotWrittenIfNotSet(c *gc.C) {
   794  	environConfig := minimalConfig(c)
   795  	machineCfg := s.createMachineConfig(c, environConfig)
   796  	cloudcfg := coreCloudinit.New()
   797  	err := cloudinit.Configure(machineCfg, cloudcfg)
   798  	c.Assert(err, gc.IsNil)
   799  
   800  	cmds := cloudcfg.BootCmds()
   801  	c.Assert(cmds, jc.DeepEquals, []interface{}{})
   802  }
   803  
   804  func (s *cloudinitSuite) TestAptProxyWritten(c *gc.C) {
   805  	environConfig := minimalConfig(c)
   806  	environConfig, err := environConfig.Apply(map[string]interface{}{
   807  		"apt-http-proxy": "http://user@10.0.0.1",
   808  	})
   809  	c.Assert(err, gc.IsNil)
   810  	machineCfg := s.createMachineConfig(c, environConfig)
   811  	cloudcfg := coreCloudinit.New()
   812  	err = cloudinit.Configure(machineCfg, cloudcfg)
   813  	c.Assert(err, gc.IsNil)
   814  
   815  	cmds := cloudcfg.BootCmds()
   816  	expected := "[ -f /etc/apt/apt.conf.d/42-juju-proxy-settings ] || (printf '%s\\n' 'Acquire::http::Proxy \"http://user@10.0.0.1\";' > /etc/apt/apt.conf.d/42-juju-proxy-settings)"
   817  	c.Assert(cmds, jc.DeepEquals, []interface{}{expected})
   818  }
   819  
   820  func (s *cloudinitSuite) TestProxyWritten(c *gc.C) {
   821  	environConfig := minimalConfig(c)
   822  	environConfig, err := environConfig.Apply(map[string]interface{}{
   823  		"http-proxy": "http://user@10.0.0.1",
   824  		"no-proxy":   "localhost,10.0.3.1",
   825  	})
   826  	c.Assert(err, gc.IsNil)
   827  	machineCfg := s.createMachineConfig(c, environConfig)
   828  	cloudcfg := coreCloudinit.New()
   829  	err = cloudinit.Configure(machineCfg, cloudcfg)
   830  	c.Assert(err, gc.IsNil)
   831  
   832  	cmds := cloudcfg.RunCmds()
   833  	first := `([ ! -e /home/ubuntu/.profile ] || grep -q '.juju-proxy' /home/ubuntu/.profile) || printf '\n# Added by juju\n[ -f "$HOME/.juju-proxy" ] && . "$HOME/.juju-proxy"\n' >> /home/ubuntu/.profile`
   834  	expected := []interface{}{
   835  		`export http_proxy=http://user@10.0.0.1`,
   836  		`export HTTP_PROXY=http://user@10.0.0.1`,
   837  		`export no_proxy=localhost,10.0.3.1`,
   838  		`export NO_PROXY=localhost,10.0.3.1`,
   839  		`[ -e /home/ubuntu ] && (printf '%s\n' 'export http_proxy=http://user@10.0.0.1
   840  export HTTP_PROXY=http://user@10.0.0.1
   841  export no_proxy=localhost,10.0.3.1
   842  export NO_PROXY=localhost,10.0.3.1' > /home/ubuntu/.juju-proxy && chown ubuntu:ubuntu /home/ubuntu/.juju-proxy)`,
   843  	}
   844  	found := false
   845  	for i, cmd := range cmds {
   846  		if cmd == first {
   847  			c.Assert(cmds[i+1:i+6], jc.DeepEquals, expected)
   848  			found = true
   849  			break
   850  		}
   851  	}
   852  	c.Assert(found, jc.IsTrue)
   853  }
   854  
   855  var serverCert = []byte(`
   856  SERVER CERT
   857  -----BEGIN CERTIFICATE-----
   858  MIIBdzCCASOgAwIBAgIBADALBgkqhkiG9w0BAQUwHjENMAsGA1UEChMEanVqdTEN
   859  MAsGA1UEAxMEcm9vdDAeFw0xMjExMDgxNjIyMzRaFw0xMzExMDgxNjI3MzRaMBwx
   860  DDAKBgNVBAoTA2htbTEMMAoGA1UEAxMDYW55MFowCwYJKoZIhvcNAQEBA0sAMEgC
   861  QQCACqz6JPwM7nbxAWub+APpnNB7myckWJ6nnsPKi9SipP1hyhfzkp8RGMJ5Uv7y
   862  8CSTtJ8kg/ibka1VV8LvP9tnAgMBAAGjUjBQMA4GA1UdDwEB/wQEAwIAsDAdBgNV
   863  HQ4EFgQU6G1ERaHCgfAv+yoDMFVpDbLOmIQwHwYDVR0jBBgwFoAUP/mfUdwOlHfk
   864  fR+gLQjslxf64w0wCwYJKoZIhvcNAQEFA0EAbn0MaxWVgGYBomeLYfDdb8vCq/5/
   865  G/2iCUQCXsVrBparMLFnor/iKOkJB5n3z3rtu70rFt+DpX6L8uBR3LB3+A==
   866  -----END CERTIFICATE-----
   867  `[1:])
   868  
   869  var serverKey = []byte(`
   870  SERVER KEY
   871  -----BEGIN RSA PRIVATE KEY-----
   872  MIIBPAIBAAJBAIAKrPok/AzudvEBa5v4A+mc0HubJyRYnqeew8qL1KKk/WHKF/OS
   873  nxEYwnlS/vLwJJO0nySD+JuRrVVXwu8/22cCAwEAAQJBAJsk1F0wTRuaIhJ5xxqw
   874  FIWPFep/n5jhrDOsIs6cSaRbfIBy3rAl956pf/MHKvf/IXh7KlG9p36IW49hjQHK
   875  7HkCIQD2CqyV1ppNPFSoCI8mSwO8IZppU3i2V4MhpwnqHz3H0wIhAIU5XIlhLJW8
   876  TNOaFMEia/TuYofdwJnYvi9t0v4UKBWdAiEA76AtvjEoTpi3in/ri0v78zp2/KXD
   877  JzPMDvZ0fYS30ukCIA1stlJxpFiCXQuFn0nG+jH4Q52FTv8xxBhrbLOFvHRRAiEA
   878  2Vc9NN09ty+HZgxpwqIA1fHVuYJY9GMPG1LnTnZ9INg=
   879  -----END RSA PRIVATE KEY-----
   880  `[1:])