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 = ¶ms.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:])