func (t *localServerSuite) TestConstraintsMerge(c *gc.C) { env := t.Prepare(c) validator, err := env.ConstraintsValidator() c.Assert(err, gc.IsNil) consA := constraints.MustParse("arch=amd64 mem=1G cpu-power=10 cpu-cores=2 tags=bar") consB := constraints.MustParse("arch=i386 instance-type=m1.small") cons, err := validator.Merge(consA, consB) c.Assert(err, gc.IsNil) c.Assert(cons, gc.DeepEquals, constraints.MustParse("arch=i386 instance-type=m1.small tags=bar")) }
func (t *localServerSuite) TestConstraintsValidatorVocab(c *gc.C) { env := t.Prepare(c) validator, err := env.ConstraintsValidator() c.Assert(err, gc.IsNil) cons := constraints.MustParse("arch=ppc64") _, err = validator.Validate(cons) c.Assert(err, gc.ErrorMatches, "invalid constraint value: arch=ppc64\nvalid values are:.*") cons = constraints.MustParse("instance-type=foo") _, err = validator.Validate(cons) c.Assert(err, gc.ErrorMatches, "invalid constraint value: instance-type=foo\nvalid values are:.*") }
func (t *localServerSuite) TestPrecheckInstanceInvalidInstanceType(c *gc.C) { env := t.Prepare(c) cons := constraints.MustParse("instance-type=m1.invalid") placement := "" err := env.PrecheckInstance("precise", cons, placement) c.Assert(err, gc.ErrorMatches, `invalid AWS instance type "m1.invalid" specified`) }
func (t *localServerSuite) TestPrecheckInstanceValidInstanceType(c *gc.C) { env := t.Prepare(c) cons := constraints.MustParse("instance-type=m1.small root-disk=1G") placement := "" err := env.PrecheckInstance("precise", cons, placement) c.Assert(err, gc.IsNil) }
func (s *getSuite) TestServiceGet(c *gc.C) { for i, t := range getTests { c.Logf("test %d. %s", i, t.about) ch := s.AddTestingCharm(c, t.charm) svc := s.AddTestingService(c, fmt.Sprintf("test%d", i), ch) var constraintsv constraints.Value if t.constraints != "" { constraintsv = constraints.MustParse(t.constraints) err := svc.SetConstraints(constraintsv) c.Assert(err, gc.IsNil) } if t.config != nil { err := svc.UpdateConfigSettings(t.config) c.Assert(err, gc.IsNil) } expect := t.expect expect.Constraints = constraintsv expect.Service = svc.Name() expect.Charm = ch.Meta().Name apiclient := s.APIState.Client() got, err := apiclient.ServiceGet(svc.Name()) c.Assert(err, gc.IsNil) c.Assert(*got, gc.DeepEquals, expect) } }
func (t *localServerSuite) TestPrecheckInstanceUnsupportedArch(c *gc.C) { env := t.Prepare(c) cons := constraints.MustParse("instance-type=cc1.4xlarge arch=i386") placement := "" err := env.PrecheckInstance("precise", cons, placement) c.Assert(err, gc.ErrorMatches, `invalid AWS instance type "cc1.4xlarge" and arch "i386" specified`) }
func (s *instanceTypeSuite) TestPrecheckInstanceInvalidInstanceType(c *gc.C) { env := s.setupEnvWithDummyMetadata(c) cons := constraints.MustParse("instance-type=Super") placement := "" err := env.PrecheckInstance("precise", cons, placement) c.Assert(err, gc.ErrorMatches, `invalid Azure instance "Super" specified`) }
func (s *instanceTypeSuite) TestPrecheckInstanceValidInstanceType(c *gc.C) { env := s.setupEnvWithDummyMetadata(c) cons := constraints.MustParse("instance-type=Large") placement := "" err := env.PrecheckInstance("precise", cons, placement) c.Assert(err, gc.IsNil) }
func (s *instanceTypeSuite) TestFindInstanceSpec(c *gc.C) { env := s.setupEnvWithDummyMetadata(c) for i, t := range findInstanceSpecTests { c.Logf("test %d", i) cons := constraints.MustParse(t.cons) constraints := &instances.InstanceConstraint{ Region: "West US", Series: t.series, Arches: []string{"amd64"}, Constraints: cons, } // Find a matching instance type and image. spec, err := findInstanceSpec(env, constraints) c.Assert(err, gc.IsNil) // We got the instance type we described in our constraints, and // the image returned by (the fake) simplestreams. if cons.HasInstanceType() { c.Check(spec.InstanceType.Name, gc.Equals, *cons.InstanceType) } else { c.Check(spec.InstanceType.Name, gc.Equals, t.itype) } c.Check(spec.Image.Id, gc.Equals, "image-id") } }
func (suite *environSuite) TestConstraintsValidatorVocab(c *gc.C) { suite.setupFakeImageMetadata(c) env := suite.makeEnviron() validator, err := env.ConstraintsValidator() c.Assert(err, gc.IsNil) cons := constraints.MustParse("arch=ppc64") _, err = validator.Validate(cons) c.Assert(err, gc.ErrorMatches, "invalid constraint value: arch=ppc64\nvalid values are:.*") }
func (t *localServerSuite) TestConstraintsValidatorUnsupported(c *gc.C) { env := t.Prepare(c) validator, err := env.ConstraintsValidator() c.Assert(err, gc.IsNil) cons := constraints.MustParse("arch=amd64 tags=foo") unsupported, err := validator.Validate(cons) c.Assert(err, gc.IsNil) c.Assert(unsupported, gc.DeepEquals, []string{"tags"}) }
func (suite *environSuite) TestConstraintsValidator(c *gc.C) { suite.setupFakeImageMetadata(c) env := suite.makeEnviron() validator, err := env.ConstraintsValidator() c.Assert(err, gc.IsNil) cons := constraints.MustParse("arch=amd64 cpu-power=10 instance-type=foo") unsupported, err := validator.Validate(cons) c.Assert(err, gc.IsNil) c.Assert(unsupported, jc.SameContents, []string{"cpu-power", "instance-type"}) }
func FindInstanceSpec(e environs.Environ, series, arch, cons string) (spec *instances.InstanceSpec, err error) { env := e.(*environ) spec, err = findInstanceSpec(env, &instances.InstanceConstraint{ Series: series, Arches: []string{arch}, Region: env.ecfg().region(), Constraints: constraints.MustParse(cons), }) return }
func (s *DeploySuite) TestConstraints(c *gc.C) { coretesting.Charms.BundlePath(s.SeriesPath, "dummy") err := runDeploy(c, "local:dummy", "--constraints", "mem=2G cpu-cores=2") c.Assert(err, gc.IsNil) curl := charm.MustParseURL("local:precise/dummy-1") service, _ := s.AssertService(c, "dummy", curl, 1, 0) cons, err := service.Constraints() c.Assert(err, gc.IsNil) c.Assert(cons, gc.DeepEquals, constraints.MustParse("mem=2G cpu-cores=2")) }
func (s *BootstrapSuite) TestValidateConstraintsCalledWithoutMetadatasource(c *gc.C) { validateCalled := 0 s.PatchValue(&validateConstraints, func(cons constraints.Value, env environs.Environ) error { c.Assert(cons, gc.DeepEquals, constraints.MustParse("mem=4G")) validateCalled++ return nil }) resetJujuHome(c) _, err := coretesting.RunCommand( c, envcmd.Wrap(&BootstrapCommand{}), "--constraints", "mem=4G") c.Assert(err, gc.IsNil) c.Assert(validateCalled, gc.Equals, 1) }
func (s *ProvisionerSuite) TestConstraints(c *gc.C) { // Create a machine with non-standard constraints. m, err := s.addMachine() c.Assert(err, gc.IsNil) cons := constraints.MustParse("mem=8G arch=amd64 cpu-cores=2 root-disk=10G") err = m.SetConstraints(cons) c.Assert(err, gc.IsNil) // Start a provisioner and check those constraints are used. p := s.newEnvironProvisioner(c) defer stop(c, p) s.checkStartInstanceCustom(c, m, "pork", cons, nil, nil, nil, true) }
func (s *CloudInitSuite) TestFinishBootstrapConfig(c *gc.C) { attrs := dummySampleConfig().Merge(testing.Attrs{ "authorized-keys": "we-are-the-keys", "admin-secret": "lisboan-pork", "agent-version": "1.2.3", "state-server": false, }) cfg, err := config.New(config.NoDefaults, attrs) c.Assert(err, gc.IsNil) oldAttrs := cfg.AllAttrs() mcfg := &cloudinit.MachineConfig{ Bootstrap: true, } cons := constraints.MustParse("mem=1T cpu-power=999999999") err = environs.FinishMachineConfig(mcfg, cfg, cons) c.Assert(err, gc.IsNil) c.Check(mcfg.AuthorizedKeys, gc.Equals, "we-are-the-keys") c.Check(mcfg.DisableSSLHostnameVerification, jc.IsFalse) password := utils.UserPasswordHash("lisboan-pork", utils.CompatSalt) c.Check(mcfg.APIInfo, gc.DeepEquals, &api.Info{ Password: password, CACert: testing.CACert, }) c.Check(mcfg.StateInfo, gc.DeepEquals, &state.Info{ Password: password, CACert: testing.CACert, }) c.Check(mcfg.StateServingInfo.StatePort, gc.Equals, cfg.StatePort()) c.Check(mcfg.StateServingInfo.APIPort, gc.Equals, cfg.APIPort()) c.Check(mcfg.Constraints, gc.DeepEquals, cons) oldAttrs["ca-private-key"] = "" oldAttrs["admin-secret"] = "" c.Check(mcfg.Config.AllAttrs(), gc.DeepEquals, oldAttrs) srvCertPEM := mcfg.StateServingInfo.Cert srvKeyPEM := mcfg.StateServingInfo.PrivateKey _, _, err = cert.ParseCertAndKey(srvCertPEM, srvKeyPEM) c.Check(err, gc.IsNil) err = cert.Verify(srvCertPEM, testing.CACert, time.Now()) c.Assert(err, gc.IsNil) err = cert.Verify(srvCertPEM, testing.CACert, time.Now().AddDate(9, 0, 0)) c.Assert(err, gc.IsNil) err = cert.Verify(srvCertPEM, testing.CACert, time.Now().AddDate(10, 0, 1)) c.Assert(err, gc.NotNil) }
func (s *BootstrapSuite) TestValidateConstraintsCalledWithMetadatasource(c *gc.C) { sourceDir, _ := createImageMetadata(c) resetJujuHome(c) var calledFuncs []string s.PatchValue(&uploadCustomMetadata, func(metadataDir string, env environs.Environ) error { c.Assert(metadataDir, gc.DeepEquals, sourceDir) calledFuncs = append(calledFuncs, "uploadCustomMetadata") return nil }) s.PatchValue(&validateConstraints, func(cons constraints.Value, env environs.Environ) error { c.Assert(cons, gc.DeepEquals, constraints.MustParse("mem=4G")) calledFuncs = append(calledFuncs, "validateConstraints") return nil }) _, err := coretesting.RunCommand( c, envcmd.Wrap(&BootstrapCommand{}), "--metadata-source", sourceDir, "--constraints", "mem=4G") c.Assert(err, gc.IsNil) c.Assert(calledFuncs, gc.DeepEquals, []string{"uploadCustomMetadata", "validateConstraints"}) }
func (t *LiveTests) BootstrapOnce(c *gc.C) { if t.bootstrapped { return } t.PrepareOnce(c) // We only build and upload tools if there will be a state agent that // we could connect to (actual live tests, rather than local-only) cons := constraints.MustParse("mem=2G") if t.CanOpenState { _, err := sync.Upload(t.Env.Storage(), nil, coretesting.FakeDefaultSeries) c.Assert(err, gc.IsNil) } t.UploadFakeTools(c, t.Env.Storage()) err := bootstrap.EnsureNotBootstrapped(t.Env) c.Assert(err, gc.IsNil) err = bootstrap.Bootstrap(coretesting.Context(c), t.Env, environs.BootstrapParams{Constraints: cons}) c.Assert(err, gc.IsNil) t.bootstrapped = true }
func (s *CommonProvisionerSuite) SetUpSuite(c *gc.C) { s.JujuConnSuite.SetUpSuite(c) s.defaultConstraints = constraints.MustParse("arch=amd64 mem=4G cpu-cores=1 root-disk=8G") }
// setUpScenario adds some entities to the state so that // we can check that they all get pulled in by // allWatcherStateBacking.getAll. func (s *storeManagerStateSuite) setUpScenario(c *gc.C) (entities entityInfoSlice) { add := func(e params.EntityInfo) { entities = append(entities, e) } m, err := s.State.AddMachine("quantal", JobManageEnviron) c.Assert(err, gc.IsNil) c.Assert(m.Tag(), gc.Equals, "machine-0") err = m.SetProvisioned(instance.Id("i-"+m.Tag()), "fake_nonce", nil) c.Assert(err, gc.IsNil) hc, err := m.HardwareCharacteristics() c.Assert(err, gc.IsNil) err = m.SetAddresses(instance.NewAddress("example.com", instance.NetworkUnknown)) c.Assert(err, gc.IsNil) add(¶ms.MachineInfo{ Id: "0", InstanceId: "i-machine-0", Status: params.StatusPending, Life: params.Alive, Series: "quantal", Jobs: []params.MachineJob{JobManageEnviron.ToParams()}, Addresses: m.Addresses(), HardwareCharacteristics: hc, }) wordpress := AddTestingService(c, s.State, "wordpress", AddTestingCharm(c, s.State, "wordpress")) err = wordpress.SetExposed() c.Assert(err, gc.IsNil) err = wordpress.SetMinUnits(3) c.Assert(err, gc.IsNil) err = wordpress.SetConstraints(constraints.MustParse("mem=100M")) c.Assert(err, gc.IsNil) setServiceConfigAttr(c, wordpress, "blog-title", "boring") add(¶ms.ServiceInfo{ Name: "wordpress", Exposed: true, CharmURL: serviceCharmURL(wordpress).String(), OwnerTag: "user-admin", Life: params.Alive, MinUnits: 3, Constraints: constraints.MustParse("mem=100M"), Config: charm.Settings{"blog-title": "boring"}, }) pairs := map[string]string{"x": "12", "y": "99"} err = wordpress.SetAnnotations(pairs) c.Assert(err, gc.IsNil) add(¶ms.AnnotationInfo{ Tag: "service-wordpress", Annotations: pairs, }) logging := AddTestingService(c, s.State, "logging", AddTestingCharm(c, s.State, "logging")) add(¶ms.ServiceInfo{ Name: "logging", CharmURL: serviceCharmURL(logging).String(), OwnerTag: "user-admin", Life: params.Alive, Config: charm.Settings{}, }) eps, err := s.State.InferEndpoints([]string{"logging", "wordpress"}) c.Assert(err, gc.IsNil) rel, err := s.State.AddRelation(eps...) c.Assert(err, gc.IsNil) add(¶ms.RelationInfo{ Key: "logging:logging-directory wordpress:logging-dir", Id: rel.Id(), Endpoints: []params.Endpoint{ {ServiceName: "logging", Relation: charm.Relation{Name: "logging-directory", Role: "requirer", Interface: "logging", Optional: false, Limit: 1, Scope: "container"}}, {ServiceName: "wordpress", Relation: charm.Relation{Name: "logging-dir", Role: "provider", Interface: "logging", Optional: false, Limit: 0, Scope: "container"}}}, }) for i := 0; i < 2; i++ { wu, err := wordpress.AddUnit() c.Assert(err, gc.IsNil) c.Assert(wu.Tag(), gc.Equals, fmt.Sprintf("unit-wordpress-%d", i)) m, err := s.State.AddMachine("quantal", JobHostUnits) c.Assert(err, gc.IsNil) c.Assert(m.Tag(), gc.Equals, fmt.Sprintf("machine-%d", i+1)) add(¶ms.UnitInfo{ Name: fmt.Sprintf("wordpress/%d", i), Service: wordpress.Name(), Series: m.Series(), MachineId: m.Id(), Ports: []instance.Port{}, Status: params.StatusPending, }) pairs := map[string]string{"name": fmt.Sprintf("bar %d", i)} err = wu.SetAnnotations(pairs) c.Assert(err, gc.IsNil) add(¶ms.AnnotationInfo{ Tag: fmt.Sprintf("unit-wordpress-%d", i), Annotations: pairs, }) err = m.SetProvisioned(instance.Id("i-"+m.Tag()), "fake_nonce", nil) c.Assert(err, gc.IsNil) err = m.SetStatus(params.StatusError, m.Tag(), nil) c.Assert(err, gc.IsNil) hc, err := m.HardwareCharacteristics() c.Assert(err, gc.IsNil) add(¶ms.MachineInfo{ Id: fmt.Sprint(i + 1), InstanceId: "i-" + m.Tag(), Status: params.StatusError, StatusInfo: m.Tag(), Life: params.Alive, Series: "quantal", Jobs: []params.MachineJob{JobHostUnits.ToParams()}, Addresses: []instance.Address{}, HardwareCharacteristics: hc, }) err = wu.AssignToMachine(m) c.Assert(err, gc.IsNil) deployer, ok := wu.DeployerTag() c.Assert(ok, gc.Equals, true) c.Assert(deployer, gc.Equals, fmt.Sprintf("machine-%d", i+1)) wru, err := rel.Unit(wu) c.Assert(err, gc.IsNil) // Create the subordinate unit as a side-effect of entering // scope in the principal's relation-unit. err = wru.EnterScope(nil) c.Assert(err, gc.IsNil) lu, err := s.State.Unit(fmt.Sprintf("logging/%d", i)) c.Assert(err, gc.IsNil) c.Assert(lu.IsPrincipal(), gc.Equals, false) deployer, ok = lu.DeployerTag() c.Assert(ok, gc.Equals, true) c.Assert(deployer, gc.Equals, fmt.Sprintf("unit-wordpress-%d", i)) add(¶ms.UnitInfo{ Name: fmt.Sprintf("logging/%d", i), Service: "logging", Series: "quantal", Ports: []instance.Port{}, Status: params.StatusPending, }) } return }
Exposed: true, CharmURL: "local:quantal/quantal-wordpress-3", OwnerTag: "user-admin", Life: params.Alive, MinUnits: 42, Config: charm.Settings{}, }, }, }, { about: "service is updated if it's in backing and in multiwatcher.Store", add: []params.EntityInfo{¶ms.ServiceInfo{ Name: "wordpress", Exposed: true, CharmURL: "local:quantal/quantal-wordpress-3", MinUnits: 47, Constraints: constraints.MustParse("mem=99M"), Config: charm.Settings{"blog-title": "boring"}, }}, setUp: func(c *gc.C, st *State) { svc := AddTestingService(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) setServiceConfigAttr(c, svc, "blog-title", "boring") }, change: watcher.Change{ C: "services", Id: "wordpress", }, expectContents: []params.EntityInfo{ ¶ms.ServiceInfo{ Name: "wordpress", CharmURL: "local:quantal/quantal-wordpress-3", OwnerTag: "user-admin",
func (s *ConstraintsSuite) TestDefaults(c *gc.C) { for _, test := range []struct { cons string expected kvm.StartParams infoLog []string }{{ expected: kvm.StartParams{ Memory: kvm.DefaultMemory, CpuCores: kvm.DefaultCpu, RootDisk: kvm.DefaultDisk, }, }, { cons: "mem=256M", expected: kvm.StartParams{ Memory: kvm.MinMemory, CpuCores: kvm.DefaultCpu, RootDisk: kvm.DefaultDisk, }, }, { cons: "mem=4G", expected: kvm.StartParams{ Memory: 4 * 1024, CpuCores: kvm.DefaultCpu, RootDisk: kvm.DefaultDisk, }, }, { cons: "cpu-cores=4", expected: kvm.StartParams{ Memory: kvm.DefaultMemory, CpuCores: 4, RootDisk: kvm.DefaultDisk, }, }, { cons: "cpu-cores=0", expected: kvm.StartParams{ Memory: kvm.DefaultMemory, CpuCores: kvm.MinCpu, RootDisk: kvm.DefaultDisk, }, }, { cons: "root-disk=512M", expected: kvm.StartParams{ Memory: kvm.DefaultMemory, CpuCores: kvm.DefaultCpu, RootDisk: kvm.MinDisk, }, }, { cons: "root-disk=4G", expected: kvm.StartParams{ Memory: kvm.DefaultMemory, CpuCores: kvm.DefaultCpu, RootDisk: 4, }, }, { cons: "arch=armhf", expected: kvm.StartParams{ Memory: kvm.DefaultMemory, CpuCores: kvm.DefaultCpu, RootDisk: kvm.DefaultDisk, }, infoLog: []string{ `arch constraint of "armhf" being ignored as not supported`, }, }, { cons: "container=lxc", expected: kvm.StartParams{ Memory: kvm.DefaultMemory, CpuCores: kvm.DefaultCpu, RootDisk: kvm.DefaultDisk, }, infoLog: []string{ `container constraint of "lxc" being ignored as not supported`, }, }, { cons: "cpu-power=100", expected: kvm.StartParams{ Memory: kvm.DefaultMemory, CpuCores: kvm.DefaultCpu, RootDisk: kvm.DefaultDisk, }, infoLog: []string{ `cpu-power constraint of 100 being ignored as not supported`, }, }, { cons: "tags=foo,bar", expected: kvm.StartParams{ Memory: kvm.DefaultMemory, CpuCores: kvm.DefaultCpu, RootDisk: kvm.DefaultDisk, }, infoLog: []string{ `tags constraint of "foo,bar" being ignored as not supported`, }, }, { cons: "mem=4G cpu-cores=4 root-disk=20G arch=armhf cpu-power=100 container=lxc tags=foo,bar", expected: kvm.StartParams{ Memory: 4 * 1024, CpuCores: 4, RootDisk: 20, }, infoLog: []string{ `arch constraint of "armhf" being ignored as not supported`, `container constraint of "lxc" being ignored as not supported`, `cpu-power constraint of 100 being ignored as not supported`, `tags constraint of "foo,bar" being ignored as not supported`, }, }} { tw := &loggo.TestWriter{} c.Assert(loggo.RegisterWriter("constraint-tester", tw, loggo.DEBUG), gc.IsNil) cons := constraints.MustParse(test.cons) params := kvm.ParseConstraintsToStartParams(cons) c.Check(params, gc.DeepEquals, test.expected) c.Check(tw.Log, jc.LogMatches, test.infoLog) loggo.RemoveWriter("constraint-tester") } }
info: "lonely --upload-series", args: []string{"--upload-series", "fine"}, err: `--upload-series requires --upload-tools`, }, { info: "--upload-series with --series", args: []string{"--upload-tools", "--upload-series", "foo", "--series", "bar"}, err: `--upload-series and --series can't be used together`, }, { info: "bad environment", version: "1.2.3-%LTS%-amd64", args: []string{"-e", "brokenenv"}, err: `dummy.Bootstrap is broken`, }, { info: "constraints", args: []string{"--constraints", "mem=4G cpu-cores=4"}, constraints: constraints.MustParse("mem=4G cpu-cores=4"), }, { info: "unsupported constraint passed through but no error", args: []string{"--constraints", "mem=4G cpu-cores=4 cpu-power=10"}, constraints: constraints.MustParse("mem=4G cpu-cores=4 cpu-power=10"), }, { info: "--upload-tools picks all reasonable series", version: "1.2.3-saucy-amd64", args: []string{"--upload-tools"}, uploads: []string{ "1.2.3.1-saucy-amd64", // from version.Current "1.2.3.1-raring-amd64", // from env.Config().DefaultSeries() "1.2.3.1-precise-amd64", "1.2.3.1-trusty-amd64", }, }, {
Addresses: []instance.Address{}, HardwareCharacteristics: &instance.HardwareCharacteristics{}, }, }, json: `["machine","change",{"Id":"Benji","InstanceId":"Shazam","Status":"error","StatusInfo":"foo","StatusData":null,"Life":"alive","Series":"trusty","SupportedContainers":["lxc"],"SupportedContainersKnown":false,"Jobs":["JobManageEnviron"],"Addresses":[],"HardwareCharacteristics":{}}]`, }, { about: "ServiceInfo Delta", value: params.Delta{ Entity: ¶ms.ServiceInfo{ Name: "Benji", Exposed: true, CharmURL: "cs:quantal/name", Life: params.Dying, OwnerTag: "test-owner", MinUnits: 42, Constraints: constraints.MustParse("arch=armhf mem=1024M"), Config: charm.Settings{ "hello": "goodbye", "foo": false, }, }, }, json: `["service","change",{"CharmURL": "cs:quantal/name","Name":"Benji","Exposed":true,"Life":"dying","OwnerTag":"test-owner","MinUnits":42,"Constraints":{"arch":"armhf", "mem": 1024},"Config": {"hello":"goodbye","foo":false}}]`, }, { about: "UnitInfo Delta", value: params.Delta{ Entity: ¶ms.UnitInfo{ Name: "Benji", Service: "Shazam", Series: "precise", CharmURL: "cs:~user/precise/wordpress-42",