launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #15436
[Merge] lp:~julian-edwards/juju-core/mpv-conflicts into lp:~maas-maintainers/juju-core/maas-provider-skeleton
Julian Edwards has proposed merging lp:~julian-edwards/juju-core/mpv-conflicts into lp:~maas-maintainers/juju-core/maas-provider-skeleton.
Commit message:
Commit the latest round of fixes into the integration branch.
Requested reviews:
MAAS Maintainers (maas-maintainers)
For more details, see:
https://code.launchpad.net/~julian-edwards/juju-core/mpv-conflicts/+merge/157598
Commit the latest round of fixes into the integration branch.
--
The attached diff has been truncated due to its size.
https://code.launchpad.net/~julian-edwards/juju-core/mpv-conflicts/+merge/157598
Your team MAAS Maintainers is requested to review the proposed merge of lp:~julian-edwards/juju-core/mpv-conflicts into lp:~maas-maintainers/juju-core/maas-provider-skeleton.
=== modified file 'README'
--- README 2013-02-21 14:09:17 +0000
+++ README 2013-04-08 08:09:22 +0000
@@ -40,14 +40,14 @@
will define and create `$HOME/work` as your local `GOPATH`. The `go` tool itself
will create three subdirectories inside your `GOPATH` when required; `src`, `pkg`
and `bin`, which hold the source of Go programs, compiled packages and compiled
-binaries, respectively. You should add `$GOPATH/bin` to your `PATH`.
-
-Setting `GOPATH` correctly is critical when developing Go programs. `GOPATH`
-should be exported as part of your login script. You can check your `GOPATH`
-with to `go` tool itself.
-
- % go env | grep GOPATH
- GOPATH="/home/dfc/work"
+binaries, respectively.
+
+Setting `GOPATH` correctly is critical when developing Go programs. Set and
+export it as part of your login script.
+
+Add `$GOPATH/bin` to your `PATH`, so you can run the go programs you install:
+
+ PATH="$PATH:$GOPATH/bin"
Other prerequisites
-------------------
=== modified file 'charm/charm.go'
--- charm/charm.go 2013-01-10 17:26:55 +0000
+++ charm/charm.go 2013-04-08 08:09:22 +0000
@@ -32,7 +32,7 @@
func InferRepository(curl *URL, localRepoPath string) (repo Repository, err error) {
switch curl.Schema {
case "cs":
- repo = Store()
+ repo = Store
case "local":
if localRepoPath == "" {
return nil, errors.New("path to local repository not specified")
=== modified file 'charm/charm_test.go'
--- charm/charm_test.go 2013-02-11 05:51:12 +0000
+++ charm/charm_test.go 2013-04-08 08:09:22 +0000
@@ -49,7 +49,7 @@
case *charm.LocalRepository:
c.Assert(repo.Path, Equals, t.path)
default:
- c.Assert(repo, FitsTypeOf, charm.Store())
+ c.Assert(repo, Equals, charm.Store)
}
}
curl, err := charm.InferURL("local:whatever", "precise")
=== modified file 'charm/dir.go'
--- charm/dir.go 2013-02-15 16:51:12 +0000
+++ charm/dir.go 2013-04-08 08:09:22 +0000
@@ -185,7 +185,7 @@
if filepath.Dir(relpath) == "hooks" {
hookName := filepath.Base(relpath)
if _, ok := zp.hooks[hookName]; !fi.IsDir() && ok && mode&0100 == 0 {
- log.Printf("charm: WARNING: making %q executable in charm", path)
+ log.Warningf("charm: making %q executable in charm", path)
perm = perm | 0100
}
}
=== modified file 'charm/dir_test.go'
--- charm/dir_test.go 2013-02-19 07:06:20 +0000
+++ charm/dir_test.go 2013-04-08 08:09:22 +0000
@@ -118,9 +118,7 @@
// Bug #864164: Must complain if charm hooks aren't executable
func (s *DirSuite) TestBundleToWithNonExecutableHooks(c *C) {
- orig := log.Target
- log.Target = c
- defer func() { log.Target = orig }()
+ defer log.SetTarget(log.SetTarget(c))
hooks := []string{"install", "start", "config-changed", "upgrade-charm", "stop"}
for _, relName := range []string{"foo", "bar", "self"} {
for _, kind := range []string{"joined", "changed", "departed", "broken"} {
@@ -139,7 +137,7 @@
tlog := c.GetTestLog()
for _, hook := range hooks {
fullpath := filepath.Join(dir.Path, "hooks", hook)
- exp := fmt.Sprintf(`^(.|\n)*JUJU charm: WARNING: making "%s" executable in charm(.|\n)*$`, fullpath)
+ exp := fmt.Sprintf(`^(.|\n)*WARNING charm: making "%s" executable in charm(.|\n)*$`, fullpath)
c.Assert(tlog, Matches, exp, Commentf("hook %q was not made executable", fullpath))
}
=== modified file 'charm/export_test.go'
--- charm/export_test.go 2012-06-21 20:40:39 +0000
+++ charm/export_test.go 2013-04-08 08:09:22 +0000
@@ -10,6 +10,6 @@
return ifaceExpander(limit)
}
-func NewStore(url, path string) Repository {
- return &store{url, path}
+func NewStore(url string) *CharmStore {
+ return &CharmStore{url}
}
=== modified file 'charm/meta.go'
--- charm/meta.go 2013-02-15 16:12:31 +0000
+++ charm/meta.go 2013-04-08 08:09:22 +0000
@@ -11,7 +11,7 @@
"strings"
)
-// RelationScope describes the scope of a relation endpoint.
+// RelationScope describes the scope of a relation.
type RelationScope string
// Note that schema doesn't support custom string types,
@@ -23,9 +23,20 @@
ScopeContainer RelationScope = "container"
)
+// RelationRole defines the role of a relation.
+type RelationRole string
+
+const (
+ RoleProvider RelationRole = "provider"
+ RoleRequirer RelationRole = "requirer"
+ RolePeer RelationRole = "peer"
+)
+
// Relation represents a single relation defined in the charm
// metadata.yaml file.
type Relation struct {
+ Name string
+ Role RelationRole
Interface string
Optional bool
Limit int
@@ -44,6 +55,7 @@
Peers map[string]Relation `bson:",omitempty"`
Format int `bson:",omitempty"`
OldRevision int `bson:",omitempty"` // Obsolete
+ Categories []string `bson:",omitempty"`
}
func generateRelationHooks(relName string, allHooks map[string]bool) {
@@ -74,6 +86,18 @@
return allHooks
}
+func parseCategories(categories interface{}) []string {
+ if categories == nil {
+ return nil
+ }
+ slice := categories.([]interface{})
+ result := make([]string, 0, len(slice))
+ for _, cat := range slice {
+ result = append(result, cat.(string))
+ }
+ return result
+}
+
// ReadMeta reads the content of a metadata.yaml file and returns
// its representation.
func ReadMeta(r io.Reader) (meta *Meta, err error) {
@@ -97,10 +121,11 @@
// enough for revisions.
meta.Summary = m["summary"].(string)
meta.Description = m["description"].(string)
- meta.Provides = parseRelations(m["provides"])
- meta.Requires = parseRelations(m["requires"])
- meta.Peers = parseRelations(m["peers"])
+ meta.Provides = parseRelations(m["provides"], RoleProvider)
+ meta.Requires = parseRelations(m["requires"], RoleRequirer)
+ meta.Peers = parseRelations(m["peers"], RolePeer)
meta.Format = int(m["format"].(int64))
+ meta.Categories = parseCategories(m["categories"])
if subordinate := m["subordinate"]; subordinate != nil {
meta.Subordinate = subordinate.(bool)
}
@@ -108,19 +133,32 @@
// Obsolete
meta.OldRevision = int(m["revision"].(int64))
}
+ if err := meta.Check(); err != nil {
+ return nil, err
+ }
+ return meta, nil
+}
+// Check checks that the metadata is well-formed.
+func (meta Meta) Check() error {
// Check for duplicate or forbidden relation names or interfaces.
names := map[string]bool{}
- checkRelations := func(src map[string]Relation, isRequire bool) error {
+ checkRelations := func(src map[string]Relation, role RelationRole) error {
for name, rel := range src {
+ if rel.Name != name {
+ return fmt.Errorf("charm %q has mismatched relation name %q; expected %q", meta.Name, rel.Name, name)
+ }
+ if rel.Role != role {
+ return fmt.Errorf("charm %q has mismatched role %q; expected %q", meta.Name, rel.Role, role)
+ }
// Container-scoped require relations on subordinates are allowed
// to use the otherwise-reserved juju-* namespace.
- if !meta.Subordinate || !isRequire || rel.Scope != ScopeContainer {
+ if !meta.Subordinate || role != RoleRequirer || rel.Scope != ScopeContainer {
if reservedName(name) {
return fmt.Errorf("charm %q using a reserved relation name: %q", meta.Name, name)
}
}
- if !isRequire {
+ if role != RoleRequirer {
if reservedName(rel.Interface) {
return fmt.Errorf("charm %q relation %q using a reserved interface: %q", meta.Name, name, rel.Interface)
}
@@ -132,14 +170,14 @@
}
return nil
}
- if err := checkRelations(meta.Provides, false); err != nil {
- return nil, err
- }
- if err := checkRelations(meta.Requires, true); err != nil {
- return nil, err
- }
- if err := checkRelations(meta.Peers, false); err != nil {
- return nil, err
+ if err := checkRelations(meta.Provides, RoleProvider); err != nil {
+ return err
+ }
+ if err := checkRelations(meta.Requires, RoleRequirer); err != nil {
+ return err
+ }
+ if err := checkRelations(meta.Peers, RolePeer); err != nil {
+ return err
}
// Subordinate charms must have at least one relation that
@@ -156,26 +194,29 @@
}
}
if !valid {
- return nil, fmt.Errorf("subordinate charm %q lacks requires relation with container scope", meta.Name)
+ return fmt.Errorf("subordinate charm %q lacks \"requires\" relation with container scope", meta.Name)
}
}
- return
+ return nil
}
func reservedName(name string) bool {
return name == "juju" || strings.HasPrefix(name, "juju-")
}
-func parseRelations(relations interface{}) map[string]Relation {
+func parseRelations(relations interface{}, role RelationRole) map[string]Relation {
if relations == nil {
return nil
}
result := make(map[string]Relation)
for name, rel := range relations.(map[string]interface{}) {
relMap := rel.(map[string]interface{})
- relation := Relation{}
- relation.Interface = relMap["interface"].(string)
- relation.Optional = relMap["optional"].(bool)
+ relation := Relation{
+ Name: name,
+ Role: role,
+ Interface: relMap["interface"].(string),
+ Optional: relMap["optional"].(bool),
+ }
if scope := relMap["scope"]; scope != nil {
relation.Scope = RelationScope(scope.(string))
}
@@ -269,6 +310,7 @@
"revision": schema.Int(), // Obsolete
"format": schema.Int(),
"subordinate": schema.Bool(),
+ "categories": schema.List(schema.String()),
},
schema.Defaults{
"provides": schema.Omit,
@@ -277,5 +319,6 @@
"revision": schema.Omit,
"format": 1,
"subordinate": schema.Omit,
+ "categories": schema.Omit,
},
)
=== modified file 'charm/meta_test.go'
--- charm/meta_test.go 2013-02-14 17:09:33 +0000
+++ charm/meta_test.go 2013-04-08 08:09:22 +0000
@@ -47,6 +47,13 @@
c.Assert(err, IsNil)
c.Assert(meta.Name, Equals, "format2")
c.Assert(meta.Format, Equals, 2)
+ c.Assert(meta.Categories, HasLen, 0)
+}
+
+func (s *MetaSuite) TestReadCategory(c *C) {
+ meta, err := charm.ReadMeta(repoMeta("category"))
+ c.Assert(err, IsNil)
+ c.Assert(meta.Categories, DeepEquals, []string{"database"})
}
func (s *MetaSuite) TestSubordinate(c *C) {
@@ -60,7 +67,7 @@
hackYaml := ReadYaml(r)
hackYaml["subordinate"] = true
_, err := charm.ReadMeta(hackYaml.Reader())
- c.Assert(err, ErrorMatches, "subordinate charm \"dummy\" lacks requires relation with container scope")
+ c.Assert(err, ErrorMatches, "subordinate charm \"dummy\" lacks \"requires\" relation with container scope")
}
func (s *MetaSuite) TestScopeConstraint(c *C) {
@@ -74,28 +81,79 @@
func (s *MetaSuite) TestParseMetaRelations(c *C) {
meta, err := charm.ReadMeta(repoMeta("mysql"))
c.Assert(err, IsNil)
- c.Assert(meta.Provides["server"], Equals, charm.Relation{Interface: "mysql", Scope: charm.ScopeGlobal})
+ c.Assert(meta.Provides["server"], Equals, charm.Relation{
+ Name: "server",
+ Role: charm.RoleProvider,
+ Interface: "mysql",
+ Scope: charm.ScopeGlobal,
+ })
c.Assert(meta.Requires, IsNil)
c.Assert(meta.Peers, IsNil)
meta, err = charm.ReadMeta(repoMeta("riak"))
c.Assert(err, IsNil)
- c.Assert(meta.Provides["endpoint"], Equals, charm.Relation{Interface: "http", Scope: charm.ScopeGlobal})
- c.Assert(meta.Provides["admin"], Equals, charm.Relation{Interface: "http", Scope: charm.ScopeGlobal})
- c.Assert(meta.Peers["ring"], Equals, charm.Relation{Interface: "riak", Limit: 1, Scope: charm.ScopeGlobal})
+ c.Assert(meta.Provides["endpoint"], Equals, charm.Relation{
+ Name: "endpoint",
+ Role: charm.RoleProvider,
+ Interface: "http",
+ Scope: charm.ScopeGlobal,
+ })
+ c.Assert(meta.Provides["admin"], Equals, charm.Relation{
+ Name: "admin",
+ Role: charm.RoleProvider,
+ Interface: "http",
+ Scope: charm.ScopeGlobal,
+ })
+ c.Assert(meta.Peers["ring"], Equals, charm.Relation{
+ Name: "ring",
+ Role: charm.RolePeer,
+ Interface: "riak",
+ Limit: 1,
+ Scope: charm.ScopeGlobal,
+ })
c.Assert(meta.Requires, IsNil)
meta, err = charm.ReadMeta(repoMeta("terracotta"))
c.Assert(err, IsNil)
- c.Assert(meta.Provides["dso"], Equals, charm.Relation{Interface: "terracotta", Optional: true, Scope: charm.ScopeGlobal})
- c.Assert(meta.Peers["server-array"], Equals, charm.Relation{Interface: "terracotta-server", Limit: 1, Scope: charm.ScopeGlobal})
+ c.Assert(meta.Provides["dso"], Equals, charm.Relation{
+ Name: "dso",
+ Role: charm.RoleProvider,
+ Interface: "terracotta",
+ Optional: true,
+ Scope: charm.ScopeGlobal,
+ })
+ c.Assert(meta.Peers["server-array"], Equals, charm.Relation{
+ Name: "server-array",
+ Role: charm.RolePeer,
+ Interface: "terracotta-server",
+ Limit: 1,
+ Scope: charm.ScopeGlobal,
+ })
c.Assert(meta.Requires, IsNil)
meta, err = charm.ReadMeta(repoMeta("wordpress"))
c.Assert(err, IsNil)
- c.Assert(meta.Provides["url"], Equals, charm.Relation{Interface: "http", Scope: charm.ScopeGlobal})
- c.Assert(meta.Requires["db"], Equals, charm.Relation{Interface: "mysql", Limit: 1, Scope: charm.ScopeGlobal})
- c.Assert(meta.Requires["cache"], Equals, charm.Relation{Interface: "varnish", Limit: 2, Optional: true, Scope: charm.ScopeGlobal})
+ c.Assert(meta.Provides["url"], Equals, charm.Relation{
+ Name: "url",
+ Role: charm.RoleProvider,
+ Interface: "http",
+ Scope: charm.ScopeGlobal,
+ })
+ c.Assert(meta.Requires["db"], Equals, charm.Relation{
+ Name: "db",
+ Role: charm.RoleRequirer,
+ Interface: "mysql",
+ Limit: 1,
+ Scope: charm.ScopeGlobal,
+ })
+ c.Assert(meta.Requires["cache"], Equals, charm.Relation{
+ Name: "cache",
+ Role: charm.RoleRequirer,
+ Interface: "varnish",
+ Limit: 2,
+ Optional: true,
+ Scope: charm.ScopeGlobal,
+ })
c.Assert(meta.Peers, IsNil)
}
@@ -176,6 +234,43 @@
innocuous: juju-info`, "")
}
+func (s *MetaSuite) TestCheckMismatchedRelationName(c *C) {
+ // This Check case cannot be covered by the above
+ // TestRelationsConstraints tests.
+ meta := charm.Meta{
+ Name: "foo",
+ Provides: map[string]charm.Relation{
+ "foo": {
+ Name: "foo",
+ Role: charm.RolePeer,
+ Interface: "x",
+ Limit: 1,
+ Scope: charm.ScopeGlobal,
+ },
+ },
+ }
+ err := meta.Check()
+ c.Assert(err, ErrorMatches, `charm "foo" has mismatched role "peer"; expected "provider"`)
+}
+
+func (s *MetaSuite) TestCheckMismatchedRole(c *C) {
+ // This Check case cannot be covered by the above
+ // TestRelationsConstraints tests.
+ meta := charm.Meta{
+ Name: "foo",
+ Provides: map[string]charm.Relation{
+ "foo": {
+ Role: charm.RolePeer,
+ Interface: "foo",
+ Limit: 1,
+ Scope: charm.ScopeGlobal,
+ },
+ },
+ }
+ err := meta.Check()
+ c.Assert(err, ErrorMatches, `charm "foo" has mismatched relation name ""; expected "foo"`)
+}
+
// Test rewriting of a given interface specification into long form.
//
// InterfaceExpander uses `coerce` to do one of two things:
@@ -255,3 +350,61 @@
}
c.Assert(hooks, DeepEquals, expectedHooks)
}
+
+func (s *MetaSuite) TestCodecRoundTripEmpty(c *C) {
+ for i, codec := range codecs {
+ c.Logf("codec %d", i)
+ empty_input := charm.Meta{}
+ data, err := codec.Marshal(empty_input)
+ c.Assert(err, IsNil)
+ var empty_output charm.Meta
+ err = codec.Unmarshal(data, &empty_output)
+ c.Assert(err, IsNil)
+ c.Assert(empty_input, DeepEquals, empty_output)
+ }
+}
+
+func (s *MetaSuite) TestCodecRoundTrip(c *C) {
+ var input = charm.Meta{
+ Name: "Foo",
+ Summary: "Bar",
+ Description: "Baz",
+ Subordinate: true,
+ Provides: map[string]charm.Relation{
+ "qux": {
+ Interface: "quxx",
+ Optional: true,
+ Limit: 42,
+ Scope: "quxxx",
+ },
+ },
+ Requires: map[string]charm.Relation{
+ "qux": {
+ Interface: "quxx",
+ Optional: true,
+ Limit: 42,
+ Scope: "quxxx",
+ },
+ },
+ Peers: map[string]charm.Relation{
+ "qux": {
+ Interface: "quxx",
+ Optional: true,
+ Limit: 42,
+ Scope: "quxxx",
+ },
+ },
+ Categories: []string{"quxxxx", "quxxxxx"},
+ Format: 10,
+ OldRevision: 11,
+ }
+ for i, codec := range codecs {
+ c.Logf("codec %d", i)
+ data, err := codec.Marshal(input)
+ c.Assert(err, IsNil)
+ var output charm.Meta
+ err = codec.Unmarshal(data, &output)
+ c.Assert(err, IsNil)
+ c.Assert(input, DeepEquals, output)
+ }
+}
=== modified file 'charm/repo.go'
--- charm/repo.go 2012-10-11 17:40:17 +0000
+++ charm/repo.go 2013-04-08 08:09:22 +0000
@@ -7,6 +7,7 @@
"fmt"
"io"
"io/ioutil"
+ "launchpad.net/juju-core/environs/config"
"launchpad.net/juju-core/log"
"net/http"
"net/url"
@@ -24,77 +25,134 @@
Warnings []string `json:"warnings,omitempty"`
}
+// EventResponse is sent by the charm store in response to charm-event requests.
+type EventResponse struct {
+ Kind string `json:"kind"`
+ Revision int `json:"revision"` // Zero is valid. Can't omitempty.
+ Digest string `json:"digest,omitempty"`
+ Errors []string `json:"errors,omitempty"`
+ Warnings []string `json:"warnings,omitempty"`
+ Time string `json:"time,omitempty"`
+}
+
// Repository respresents a collection of charms.
type Repository interface {
Get(curl *URL) (Charm, error)
Latest(curl *URL) (int, error)
}
-// store is a Repository that talks to the juju charm server (in ../store).
-type store struct {
- baseURL string
- cachePath string
-}
-
-const (
- storeURL = "https://store.juju.ubuntu.com"
- cachePath = "$HOME/.juju/cache"
-)
-
-// Store returns a Repository that provides access to the juju charm store.
-func Store() Repository {
- return &store{storeURL, os.ExpandEnv(cachePath)}
-}
-
-// info returns the revision and SHA256 digest of the charm referenced by curl.
-func (s *store) info(curl *URL) (rev int, digest string, err error) {
+// NotFoundError represents an error indicating that the requested data wasn't found.
+type NotFoundError struct {
+ msg string
+}
+
+func (e *NotFoundError) Error() string {
+ return e.msg
+}
+
+// CharmStore is a Repository that provides access to the public juju charm store.
+type CharmStore struct {
+ baseURL string
+}
+
+var Store = &CharmStore{"https://store.juju.ubuntu.com"}
+
+// Info returns details for a charm in the charm store.
+func (s *CharmStore) Info(curl *URL) (*InfoResponse, error) {
key := curl.String()
resp, err := http.Get(s.baseURL + "/charm-info?charms=" + url.QueryEscape(key))
if err != nil {
- return
+ return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
- return
+ return nil, err
}
infos := make(map[string]*InfoResponse)
if err = json.Unmarshal(body, &infos); err != nil {
- return
+ return nil, err
}
info, found := infos[key]
if !found {
- err = fmt.Errorf("charm: charm store returned response without charm %q", key)
- return
+ return nil, fmt.Errorf("charm: charm store returned response without charm %q", key)
+ }
+ if len(info.Errors) == 1 && info.Errors[0] == "entry not found" {
+ return nil, &NotFoundError{fmt.Sprintf("charm not found: %s", curl)}
+ }
+ return info, nil
+}
+
+// Event returns details for a charm event in the charm store.
+//
+// If digest is empty, the latest event is returned.
+func (s *CharmStore) Event(curl *URL, digest string) (*EventResponse, error) {
+ key := curl.String()
+ query := key
+ if digest != "" {
+ query += "@" + digest
+ }
+ resp, err := http.Get(s.baseURL + "/charm-event?charms=" + url.QueryEscape(query))
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+ events := make(map[string]*EventResponse)
+ if err = json.Unmarshal(body, &events); err != nil {
+ return nil, err
+ }
+ event, found := events[key]
+ if !found {
+ return nil, fmt.Errorf("charm: charm store returned response without charm %q", key)
+ }
+ if len(event.Errors) == 1 && event.Errors[0] == "entry not found" {
+ if digest == "" {
+ return nil, &NotFoundError{fmt.Sprintf("charm event not found for %q", curl)}
+ } else {
+ return nil, &NotFoundError{fmt.Sprintf("charm event not found for %q with digest %q", curl, digest)}
+ }
+ }
+ return event, nil
+}
+
+// revision returns the revision and SHA256 digest of the charm referenced by curl.
+func (s *CharmStore) revision(curl *URL) (revision int, digest string, err error) {
+ info, err := s.Info(curl)
+ if err != nil {
+ return 0, "", err
}
for _, w := range info.Warnings {
- log.Printf("charm: WARNING: charm store reports for %q: %s", key, w)
+ log.Warningf("charm: charm store reports for %q: %s", curl, w)
}
if info.Errors != nil {
- err = fmt.Errorf(
- "charm info errors for %q: %s", key, strings.Join(info.Errors, "; "),
- )
- return
+ return 0, "", fmt.Errorf("charm info errors for %q: %s", curl, strings.Join(info.Errors, "; "))
}
return info.Revision, info.Sha256, nil
}
// Latest returns the latest revision of the charm referenced by curl, regardless
// of the revision set on curl itself.
-func (s *store) Latest(curl *URL) (int, error) {
- rev, _, err := s.info(curl.WithRevision(-1))
+func (s *CharmStore) Latest(curl *URL) (int, error) {
+ rev, _, err := s.revision(curl.WithRevision(-1))
return rev, err
}
// verify returns an error unless a file exists at path with a hex-encoded
// SHA256 matching digest.
func verify(path, digest string) error {
- b, err := ioutil.ReadFile(path)
+ f, err := os.Open(path)
if err != nil {
return err
}
+ defer f.Close()
h := sha256.New()
- h.Write(b)
+ if _, err := io.Copy(h, f); err != nil {
+ return err
+ }
if hex.EncodeToString(h.Sum(nil)) != digest {
return fmt.Errorf("bad SHA256 of %q", path)
}
@@ -102,11 +160,12 @@
}
// Get returns the charm referenced by curl.
-func (s *store) Get(curl *URL) (Charm, error) {
- if err := os.MkdirAll(s.cachePath, 0755); err != nil {
+func (s *CharmStore) Get(curl *URL) (Charm, error) {
+ cachePath := config.JujuHomePath("cache")
+ if err := os.MkdirAll(cachePath, 0755); err != nil {
return nil, err
}
- rev, digest, err := s.info(curl)
+ rev, digest, err := s.revision(curl)
if err != nil {
return nil, err
}
@@ -115,14 +174,14 @@
} else if curl.Revision != rev {
return nil, fmt.Errorf("charm: store returned charm with wrong revision for %q", curl.String())
}
- path := filepath.Join(s.cachePath, Quote(curl.String())+".charm")
+ path := filepath.Join(cachePath, Quote(curl.String())+".charm")
if verify(path, digest) != nil {
resp, err := http.Get(s.baseURL + "/charm/" + url.QueryEscape(curl.Path()))
if err != nil {
return nil, err
}
defer resp.Body.Close()
- f, err := ioutil.TempFile(s.cachePath, "charm-download")
+ f, err := ioutil.TempFile(cachePath, "charm-download")
if err != nil {
return nil, err
}
@@ -167,11 +226,11 @@
}
func repoNotFound(path string) error {
- return fmt.Errorf("no repository found at %q", path)
+ return &NotFoundError{fmt.Sprintf("no repository found at %q", path)}
}
-func charmNotFound(curl *URL) error {
- return fmt.Errorf("no charms found matching %q", curl)
+func charmNotFound(curl *URL, repoPath string) error {
+ return &NotFoundError{fmt.Sprintf("no charms found matching %q in %s", curl, repoPath)}
}
func mightBeCharm(info os.FileInfo) bool {
@@ -201,7 +260,7 @@
path := filepath.Join(r.Path, curl.Series)
infos, err := ioutil.ReadDir(path)
if err != nil {
- return nil, charmNotFound(curl)
+ return nil, charmNotFound(curl, r.Path)
}
var latest Charm
for _, info := range infos {
@@ -210,7 +269,7 @@
}
chPath := filepath.Join(path, info.Name())
if ch, err := Read(chPath); err != nil {
- log.Printf("charm: WARNING: failed to load charm at %q: %s", chPath, err)
+ log.Warningf("charm: failed to load charm at %q: %s", chPath, err)
} else if ch.Meta().Name == curl.Name {
if ch.Revision() == curl.Revision {
return ch, nil
@@ -223,5 +282,5 @@
if curl.Revision == -1 && latest != nil {
return latest, nil
}
- return nil, charmNotFound(curl)
+ return nil, charmNotFound(curl, r.Path)
}
=== modified file 'charm/repo_test.go'
--- charm/repo_test.go 2013-02-15 13:34:46 +0000
+++ charm/repo_test.go 2013-04-08 08:09:22 +0000
@@ -7,6 +7,7 @@
"io/ioutil"
. "launchpad.net/gocheck"
"launchpad.net/juju-core/charm"
+ "launchpad.net/juju-core/environs/config"
"launchpad.net/juju-core/log"
"launchpad.net/juju-core/testing"
"net"
@@ -14,6 +15,7 @@
"os"
"path/filepath"
"strconv"
+ "strings"
)
type MockStore struct {
@@ -36,6 +38,9 @@
s.mux.HandleFunc("/charm-info", func(w http.ResponseWriter, r *http.Request) {
s.ServeInfo(w, r)
})
+ s.mux.HandleFunc("/charm-event", func(w http.ResponseWriter, r *http.Request) {
+ s.ServeEvent(w, r)
+ })
s.mux.HandleFunc("/charm/", func(w http.ResponseWriter, r *http.Request) {
s.ServeCharm(w, r)
})
@@ -60,17 +65,62 @@
switch curl.Name {
case "borken":
cr.Errors = append(cr.Errors, "badness")
- continue
case "unwise":
cr.Warnings = append(cr.Warnings, "foolishness")
fallthrough
- default:
+ case "good":
if curl.Revision == -1 {
cr.Revision = 23
} else {
cr.Revision = curl.Revision
}
cr.Sha256 = s.bundleSha256
+ default:
+ cr.Errors = append(cr.Errors, "entry not found")
+ }
+ }
+ data, err := json.Marshal(response)
+ if err != nil {
+ panic(err)
+ }
+ w.Header().Set("Content-Type", "application/json")
+ _, err = w.Write(data)
+ if err != nil {
+ panic(err)
+ }
+}
+
+func (s *MockStore) ServeEvent(w http.ResponseWriter, r *http.Request) {
+ r.ParseForm()
+ response := map[string]*charm.EventResponse{}
+ for _, url := range r.Form["charms"] {
+ digest := ""
+ if i := strings.Index(url, "@"); i >= 0 {
+ digest = url[i+1:]
+ url = url[:i]
+ }
+ er := &charm.EventResponse{}
+ response[url] = er
+ if digest != "" && digest != "the-digest" {
+ er.Kind = "not-found"
+ er.Errors = []string{"entry not found"}
+ continue
+ }
+ curl := charm.MustParseURL(url)
+ switch curl.Name {
+ case "borken":
+ er.Kind = "publish-error"
+ er.Errors = append(er.Errors, "badness")
+ case "unwise":
+ er.Warnings = append(er.Warnings, "foolishness")
+ fallthrough
+ case "good":
+ er.Kind = "published"
+ er.Revision = 23
+ er.Digest = "the-digest"
+ default:
+ er.Kind = "not-found"
+ er.Errors = []string{"entry not found"}
}
}
data, err := json.Marshal(response)
@@ -97,9 +147,9 @@
}
type StoreSuite struct {
- server *MockStore
- store charm.Repository
- cache string
+ server *MockStore
+ store *charm.CharmStore
+ oldJujuHome string
}
var _ = Suite(&StoreSuite{})
@@ -109,15 +159,25 @@
}
func (s *StoreSuite) SetUpTest(c *C) {
- s.cache = c.MkDir()
- s.store = charm.NewStore("http://127.0.0.1:4444", s.cache)
+ s.oldJujuHome = config.SetJujuHome(c.MkDir())
+ s.store = charm.NewStore("http://127.0.0.1:4444")
s.server.downloads = nil
}
func (s *StoreSuite) TearDownSuite(c *C) {
+ config.SetJujuHome(s.oldJujuHome)
s.server.lis.Close()
}
+func (s *StoreSuite) TestMissing(c *C) {
+ curl := charm.MustParseURL("cs:series/missing")
+ expect := `charm not found: cs:series/missing`
+ _, err := s.store.Latest(curl)
+ c.Assert(err, ErrorMatches, expect)
+ _, err = s.store.Get(curl)
+ c.Assert(err, ErrorMatches, expect)
+}
+
func (s *StoreSuite) TestError(c *C) {
curl := charm.MustParseURL("cs:series/borken")
expect := `charm info errors for "cs:series/borken": badness`
@@ -128,11 +188,9 @@
}
func (s *StoreSuite) TestWarning(c *C) {
- orig := log.Target
- log.Target = c
- defer func() { log.Target = orig }()
+ defer log.SetTarget(log.SetTarget(c))
curl := charm.MustParseURL("cs:series/unwise")
- expect := `.* JUJU charm: WARNING: charm store reports for "cs:series/unwise": foolishness` + "\n"
+ expect := `.* WARNING charm: charm store reports for "cs:series/unwise": foolishness` + "\n"
r, err := s.store.Latest(curl)
c.Assert(r, Equals, 23)
c.Assert(err, IsNil)
@@ -145,9 +203,9 @@
func (s *StoreSuite) TestLatest(c *C) {
for _, str := range []string{
- "cs:series/blah",
- "cs:series/blah-2",
- "cs:series/blah-99",
+ "cs:series/good",
+ "cs:series/good-2",
+ "cs:series/good-99",
} {
r, err := s.store.Latest(charm.MustParseURL(str))
c.Assert(r, Equals, 23)
@@ -164,8 +222,7 @@
}
func (s *StoreSuite) TestGetCacheImplicitRevision(c *C) {
- os.RemoveAll(s.cache)
- base := "cs:series/blah"
+ base := "cs:series/good"
curl := charm.MustParseURL(base)
revCurl := charm.MustParseURL(base + "-23")
ch, err := s.store.Get(curl)
@@ -177,8 +234,7 @@
}
func (s *StoreSuite) TestGetCacheExplicitRevision(c *C) {
- os.RemoveAll(s.cache)
- base := "cs:series/blah-12"
+ base := "cs:series/good-12"
curl := charm.MustParseURL(base)
ch, err := s.store.Get(curl)
c.Assert(err, IsNil)
@@ -188,11 +244,12 @@
}
func (s *StoreSuite) TestGetBadCache(c *C) {
- base := "cs:series/blah"
+ c.Assert(os.Mkdir(config.JujuHomePath("cache"), 0777), IsNil)
+ base := "cs:series/good"
curl := charm.MustParseURL(base)
revCurl := charm.MustParseURL(base + "-23")
name := charm.Quote(revCurl.String()) + ".charm"
- err := ioutil.WriteFile(filepath.Join(s.cache, name), nil, 0666)
+ err := ioutil.WriteFile(config.JujuHomePath("cache", name), nil, 0666)
c.Assert(err, IsNil)
ch, err := s.store.Get(curl)
c.Assert(err, IsNil)
@@ -202,6 +259,83 @@
s.assertCached(c, revCurl)
}
+// The following tests cover the low-level CharmStore-specific API.
+
+func (s *StoreSuite) TestInfo(c *C) {
+ curl := charm.MustParseURL("cs:series/good")
+ info, err := s.store.Info(curl)
+ c.Assert(err, IsNil)
+ c.Assert(info.Errors, IsNil)
+ c.Assert(info.Revision, Equals, 23)
+}
+
+func (s *StoreSuite) TestInfoNotFound(c *C) {
+ curl := charm.MustParseURL("cs:series/missing")
+ info, err := s.store.Info(curl)
+ c.Assert(err, ErrorMatches, `charm not found: cs:series/missing`)
+ c.Assert(info, IsNil)
+}
+
+func (s *StoreSuite) TestInfoError(c *C) {
+ curl := charm.MustParseURL("cs:series/borken")
+ info, err := s.store.Info(curl)
+ c.Assert(err, IsNil)
+ c.Assert(info.Errors, DeepEquals, []string{"badness"})
+}
+
+func (s *StoreSuite) TestInfoWarning(c *C) {
+ curl := charm.MustParseURL("cs:series/unwise")
+ info, err := s.store.Info(curl)
+ c.Assert(err, IsNil)
+ c.Assert(info.Warnings, DeepEquals, []string{"foolishness"})
+}
+
+func (s *StoreSuite) TestEvent(c *C) {
+ curl := charm.MustParseURL("cs:series/good")
+ event, err := s.store.Event(curl, "")
+ c.Assert(err, IsNil)
+ c.Assert(event.Errors, IsNil)
+ c.Assert(event.Revision, Equals, 23)
+ c.Assert(event.Digest, Equals, "the-digest")
+}
+
+func (s *StoreSuite) TestEventWithDigest(c *C) {
+ curl := charm.MustParseURL("cs:series/good")
+ event, err := s.store.Event(curl, "the-digest")
+ c.Assert(err, IsNil)
+ c.Assert(event.Errors, IsNil)
+ c.Assert(event.Revision, Equals, 23)
+ c.Assert(event.Digest, Equals, "the-digest")
+}
+
+func (s *StoreSuite) TestEventNotFound(c *C) {
+ curl := charm.MustParseURL("cs:series/missing")
+ event, err := s.store.Event(curl, "")
+ c.Assert(err, ErrorMatches, `charm event not found for "cs:series/missing"`)
+ c.Assert(event, IsNil)
+}
+
+func (s *StoreSuite) TestEventNotFoundDigest(c *C) {
+ curl := charm.MustParseURL("cs:series/good")
+ event, err := s.store.Event(curl, "missing-digest")
+ c.Assert(err, ErrorMatches, `charm event not found for "cs:series/good" with digest "missing-digest"`)
+ c.Assert(event, IsNil)
+}
+
+func (s *StoreSuite) TestEventError(c *C) {
+ curl := charm.MustParseURL("cs:series/borken")
+ event, err := s.store.Event(curl, "")
+ c.Assert(err, IsNil)
+ c.Assert(event.Errors, DeepEquals, []string{"badness"})
+}
+
+func (s *StoreSuite) TestEventWarning(c *C) {
+ curl := charm.MustParseURL("cs:series/unwise")
+ event, err := s.store.Event(curl, "")
+ c.Assert(err, IsNil)
+ c.Assert(event.Warnings, DeepEquals, []string{"foolishness"})
+}
+
type LocalRepoSuite struct {
testing.LoggingSuite
repo *charm.LocalRepository
@@ -228,13 +362,13 @@
func (s *LocalRepoSuite) TestMissingCharm(c *C) {
_, err := s.repo.Latest(charm.MustParseURL("local:series/zebra"))
- c.Assert(err, ErrorMatches, `no charms found matching "local:series/zebra"`)
+ c.Assert(err, ErrorMatches, `no charms found matching "local:series/zebra" in `+s.repo.Path)
_, err = s.repo.Get(charm.MustParseURL("local:series/zebra"))
- c.Assert(err, ErrorMatches, `no charms found matching "local:series/zebra"`)
+ c.Assert(err, ErrorMatches, `no charms found matching "local:series/zebra" in `+s.repo.Path)
_, err = s.repo.Latest(charm.MustParseURL("local:badseries/zebra"))
- c.Assert(err, ErrorMatches, `no charms found matching "local:badseries/zebra"`)
+ c.Assert(err, ErrorMatches, `no charms found matching "local:badseries/zebra" in `+s.repo.Path)
_, err = s.repo.Get(charm.MustParseURL("local:badseries/zebra"))
- c.Assert(err, ErrorMatches, `no charms found matching "local:badseries/zebra"`)
+ c.Assert(err, ErrorMatches, `no charms found matching "local:badseries/zebra" in `+s.repo.Path)
}
func (s *LocalRepoSuite) TestMissingRepo(c *C) {
@@ -281,7 +415,7 @@
c.Assert(err, IsNil)
c.Assert(rev, Equals, 2)
ch, err = s.repo.Get(badRevCurl)
- c.Assert(err, ErrorMatches, `no charms found matching "local:series/upgrade-33"`)
+ c.Assert(err, ErrorMatches, `no charms found matching "local:series/upgrade-33" in `+s.repo.Path)
}
func (s *LocalRepoSuite) TestBundle(c *C) {
@@ -312,9 +446,9 @@
c.Assert(err, IsNil)
c.Assert(ch.Revision(), Equals, 1)
c.Assert(c.GetTestLog(), Matches, `
-.* JUJU charm: WARNING: failed to load charm at ".*/series/blah": .*
-.* JUJU charm: WARNING: failed to load charm at ".*/series/blah.charm": .*
-.* JUJU charm: WARNING: failed to load charm at ".*/series/upgrade2": .*
+.* WARNING charm: failed to load charm at ".*/series/blah": .*
+.* WARNING charm: failed to load charm at ".*/series/blah.charm": .*
+.* WARNING charm: failed to load charm at ".*/series/upgrade2": .*
`[1:])
}
@@ -332,8 +466,8 @@
curl := charm.MustParseURL("local:series/dummy")
_, err = s.repo.Get(curl)
- c.Assert(err, ErrorMatches, `no charms found matching "local:series/dummy"`)
+ c.Assert(err, ErrorMatches, `no charms found matching "local:series/dummy" in `+s.repo.Path)
_, err = s.repo.Latest(curl)
- c.Assert(err, ErrorMatches, `no charms found matching "local:series/dummy"`)
+ c.Assert(err, ErrorMatches, `no charms found matching "local:series/dummy" in `+s.repo.Path)
c.Assert(c.GetTestLog(), Equals, "")
}
=== modified file 'charm/url.go'
--- charm/url.go 2012-09-21 12:01:53 +0000
+++ charm/url.go 2013-04-08 08:09:22 +0000
@@ -1,6 +1,7 @@
package charm
import (
+ "encoding/json"
"fmt"
"labix.org/v2/mgo/bson"
"regexp"
@@ -22,6 +23,17 @@
Revision int // -1 if unset, N otherwise
}
+var (
+ // ValidUser defines the user names that are valid in charm URLs.
+ ValidUser = regexp.MustCompile("^[a-z0-9][a-zA-Z0-9+.-]+$")
+
+ // ValidSeries defines the series names that are valid in charm URLs.
+ ValidSeries = regexp.MustCompile("^[a-z]+([a-z-]+[a-z])?$")
+
+ // ValidName defines the charm names that are valid in charm URLs.
+ ValidName = regexp.MustCompile("^[a-z][a-z0-9]*(-[a-z0-9]*[a-z][a-z0-9]*)*$")
+)
+
// WithRevision returns a URL equivalent to url but with Revision set
// to revision.
func (url *URL) WithRevision(revision int) *URL {
@@ -30,10 +42,6 @@
return &urlCopy
}
-var validUser = regexp.MustCompile("^[a-z0-9][a-zA-Z0-9+.-]+$")
-var validSeries = regexp.MustCompile("^[a-z]+([a-z-]+[a-z])?$")
-var validName = regexp.MustCompile("^[a-z][a-z0-9]*(-[a-z0-9]*[a-z][a-z0-9]*)*$")
-
// MustParseURL works like ParseURL, but panics in case of errors.
func MustParseURL(url string) *URL {
u, err := ParseURL(url)
@@ -67,7 +75,7 @@
return nil, fmt.Errorf("local charm URL with user name: %q", url)
}
u.User = parts[0][1:]
- if !validUser.MatchString(u.User) {
+ if !ValidUser.MatchString(u.User) {
return nil, fmt.Errorf("charm URL has invalid user name: %q", url)
}
parts = parts[1:]
@@ -79,7 +87,7 @@
}
if len(parts) == 2 {
u.Series = parts[0]
- if !validSeries.MatchString(u.Series) {
+ if !ValidSeries.MatchString(u.Series) {
return nil, fmt.Errorf("charm URL has invalid series: %q", url)
}
parts = parts[1:]
@@ -103,7 +111,7 @@
}
break
}
- if !validName.MatchString(u.Name) {
+ if !ValidName.MatchString(u.Name) {
return nil, fmt.Errorf("charm URL has invalid charm name: %q", url)
}
return u, nil
@@ -203,6 +211,28 @@
return nil
}
+var jsonNull = []byte("null")
+
+func (u *URL) MarshalJSON() ([]byte, error) {
+ if u == nil {
+ panic("cannot marshal nil *charm.URL")
+ }
+ return json.Marshal(u.String())
+}
+
+func (u *URL) UnmarshalJSON(b []byte) error {
+ var s string
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+ url, err := ParseURL(s)
+ if err != nil {
+ return err
+ }
+ *u = *url
+ return nil
+}
+
// Quote translates a charm url string into one which can be safely used
// in a file path. ASCII letters, ASCII digits, dot and dash stay the
// same; other characters are translated to their hex representation
=== modified file 'charm/url_test.go'
--- charm/url_test.go 2012-09-17 17:44:24 +0000
+++ charm/url_test.go 2013-04-08 08:09:22 +0000
@@ -1,10 +1,12 @@
package charm_test
import (
+ "encoding/json"
"fmt"
"labix.org/v2/mgo/bson"
. "launchpad.net/gocheck"
"launchpad.net/juju-core/charm"
+ "regexp"
)
type URLSuite struct{}
@@ -38,7 +40,8 @@
}
func (s *URLSuite) TestParseURL(c *C) {
- for _, t := range urlTests {
+ for i, t := range urlTests {
+ c.Logf("test %d", i)
url, err := charm.ParseURL(t.s)
comment := Commentf("ParseURL(%q)", t.s)
if t.err != "" {
@@ -70,7 +73,8 @@
}
func (s *URLSuite) TestInferURL(c *C) {
- for _, t := range inferTests {
+ for i, t := range inferTests {
+ c.Logf("test %d", i)
comment := Commentf("InferURL(%q, %q)", t.vague, "defseries")
inferred, ierr := charm.InferURL(t.vague, "defseries")
parsed, perr := charm.ParseURL(t.exact)
@@ -89,6 +93,57 @@
c.Assert(err, ErrorMatches, "cannot infer charm URL with user but no schema: .*")
}
+var validRegexpTests = []struct {
+ regexp *regexp.Regexp
+ string string
+ expect bool
+}{
+ {charm.ValidUser, "", false},
+ {charm.ValidUser, "bob", true},
+ {charm.ValidUser, "Bob", false},
+ {charm.ValidUser, "bOB", true},
+ {charm.ValidUser, "b^b", false},
+ {charm.ValidUser, "bob1", true},
+ {charm.ValidUser, "bob-1", true},
+ {charm.ValidUser, "bob+1", true},
+ {charm.ValidUser, "bob.1", true},
+ {charm.ValidUser, "1bob", true},
+ {charm.ValidUser, "1-bob", true},
+ {charm.ValidUser, "1+bob", true},
+ {charm.ValidUser, "1.bob", true},
+ {charm.ValidUser, "jim.bob+99-1.", true},
+
+ {charm.ValidName, "", false},
+ {charm.ValidName, "wordpress", true},
+ {charm.ValidName, "Wordpress", false},
+ {charm.ValidName, "word-press", true},
+ {charm.ValidName, "word press", false},
+ {charm.ValidName, "word^press", false},
+ {charm.ValidName, "-wordpress", false},
+ {charm.ValidName, "wordpress-", false},
+ {charm.ValidName, "wordpress2", true},
+ {charm.ValidName, "wordpress-2", false},
+ {charm.ValidName, "word2-press2", true},
+
+ {charm.ValidSeries, "", false},
+ {charm.ValidSeries, "precise", true},
+ {charm.ValidSeries, "Precise", false},
+ {charm.ValidSeries, "pre cise", false},
+ {charm.ValidSeries, "pre-cise", true},
+ {charm.ValidSeries, "pre^cise", false},
+ {charm.ValidSeries, "prec1se", false},
+ {charm.ValidSeries, "-precise", false},
+ {charm.ValidSeries, "precise-", false},
+ {charm.ValidSeries, "pre-c1se", false},
+}
+
+func (s *URLSuite) TestValidRegexps(c *C) {
+ for i, t := range validRegexpTests {
+ c.Logf("test %d: %s", i, t.string)
+ c.Assert(t.regexp.MatchString(t.string), Equals, t.expect)
+ }
+}
+
func (s *URLSuite) TestMustParseURL(c *C) {
url := charm.MustParseURL("cs:series/name")
c.Assert(url, DeepEquals, &charm.URL{"cs", "", "series", "name", -1})
@@ -107,21 +162,36 @@
c.Assert(other.WithRevision(1), DeepEquals, other)
}
-func (s *URLSuite) TestBSON(c *C) {
- type doc struct {
- URL *charm.URL
+var codecs = []struct {
+ Marshal func(interface{}) ([]byte, error)
+ Unmarshal func([]byte, interface{}) error
+}{{
+ Marshal: bson.Marshal,
+ Unmarshal: bson.Unmarshal,
+}, {
+ Marshal: json.Marshal,
+ Unmarshal: json.Unmarshal,
+}}
+
+func (s *URLSuite) TestCodecs(c *C) {
+ for i, codec := range codecs {
+ c.Logf("codec %d", i)
+ type doc struct {
+ URL *charm.URL
+ }
+ url := charm.MustParseURL("cs:series/name")
+ data, err := codec.Marshal(doc{url})
+ c.Assert(err, IsNil)
+ var v doc
+ err = codec.Unmarshal(data, &v)
+ c.Assert(v.URL, DeepEquals, url)
+
+ data, err = codec.Marshal(doc{})
+ c.Assert(err, IsNil)
+ err = codec.Unmarshal(data, &v)
+ c.Assert(err, IsNil)
+ c.Assert(v.URL, IsNil)
}
- url := charm.MustParseURL("cs:series/name")
- data, err := bson.Marshal(doc{url})
- c.Assert(err, IsNil)
- var v doc
- err = bson.Unmarshal(data, &v)
- c.Assert(v.URL, DeepEquals, url)
-
- data, err = bson.Marshal(doc{})
- c.Assert(err, IsNil)
- err = bson.Unmarshal(data, &v)
- c.Assert(v.URL, IsNil)
}
type QuoteSuite struct{}
=== modified file 'cmd/builddb/main.go'
--- cmd/builddb/main.go 2013-03-27 07:23:57 +0000
+++ cmd/builddb/main.go 2013-04-08 08:09:22 +0000
@@ -6,8 +6,8 @@
"launchpad.net/juju-core/environs"
"launchpad.net/juju-core/juju"
"launchpad.net/juju-core/log"
- "launchpad.net/juju-core/state"
- corelog "log"
+ "launchpad.net/juju-core/state/api/params"
+ stdlog "log"
"os"
"path/filepath"
"time"
@@ -19,9 +19,10 @@
)
func main() {
- log.Target = corelog.New(os.Stdout, "", corelog.LstdFlags)
+ log.SetTarget(stdlog.New(os.Stdout, "", stdlog.LstdFlags))
if err := build(); err != nil {
- corelog.Fatalf("error: %v", err)
+ fmt.Fprintf(os.Stderr, "%v\n", err)
+ os.Exit(1)
}
}
@@ -30,10 +31,6 @@
if err != nil {
return err
}
- err = environs.Bootstrap(environ, true, nil)
- if err != nil {
- return err
- }
conn, err := juju.NewConn(environ)
if err != nil {
return err
@@ -56,14 +53,14 @@
return err
}
- log.Printf("builddb: Waiting for unit to reach %q status...", state.UnitStarted)
+ log.Infof("builddb: Waiting for unit to reach %q status...", params.UnitStarted)
unit := units[0]
last, info, err := unit.Status()
if err != nil {
return err
}
logStatus(last, info)
- for last != state.UnitStarted {
+ for last != params.UnitStarted {
time.Sleep(2 * time.Second)
if err := unit.Refresh(); err != nil {
return err
@@ -81,15 +78,15 @@
if !ok {
return fmt.Errorf("cannot retrieve files: build unit lacks a public-address")
}
- log.Printf("builddb: Built files published at http://%s", addr)
- log.Printf("builddb: Remember to destroy the environment when you're done...")
+ log.Noticef("builddb: Built files published at http://%s", addr)
+ log.Noticef("builddb: Remember to destroy the environment when you're done...")
return nil
}
-func logStatus(status state.UnitStatus, info string) {
+func logStatus(status params.UnitStatus, info string) {
if info == "" {
- log.Printf("builddb: Unit status is %q", status)
+ log.Infof("builddb: Unit status is %q", status)
} else {
- log.Printf("builddb: Unit status is %q: %s", status, info)
+ log.Infof("builddb: Unit status is %q: %s", status, info)
}
}
=== modified file 'cmd/charmd/config.yaml'
--- cmd/charmd/config.yaml 2013-02-19 07:06:20 +0000
+++ cmd/charmd/config.yaml 2013-04-08 08:09:22 +0000
@@ -1,2 +1,2 @@
-mongo-url: localhost:60017
+mongo-url: localhost:27017
api-addr: localhost:8080
=== modified file 'cmd/charmd/main.go'
--- cmd/charmd/main.go 2013-02-19 07:06:20 +0000
+++ cmd/charmd/main.go 2013-04-08 08:09:22 +0000
@@ -13,7 +13,7 @@
)
func main() {
- log.Target = stdlog.New(os.Stdout, "", stdlog.LstdFlags)
+ log.SetTarget(stdlog.New(os.Stdout, "", stdlog.LstdFlags))
err := serve()
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
=== modified file 'cmd/charmload/main.go'
--- cmd/charmload/main.go 2013-02-19 07:06:20 +0000
+++ cmd/charmload/main.go 2013-04-08 08:09:22 +0000
@@ -13,7 +13,7 @@
)
func main() {
- log.Target = stdlog.New(os.Stdout, "", stdlog.LstdFlags)
+ log.SetTarget(stdlog.New(os.Stdout, "", stdlog.LstdFlags))
err := load()
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
=== modified file 'cmd/cmd.go'
--- cmd/cmd.go 2013-02-28 12:27:09 +0000
+++ cmd/cmd.go 2013-04-08 08:09:22 +0000
@@ -33,6 +33,17 @@
Run(ctx *Context) error
}
+// CommandBase provides the default implementation for SetFlags, Init, and Help.
+type CommandBase struct{}
+
+// SetFlags does nothing in the simplest case.
+func (c *CommandBase) SetFlags(f *gnuflag.FlagSet) {}
+
+// Init in the simplest case makes sure there are no args.
+func (c *CommandBase) Init(args []string) error {
+ return CheckEmpty(args)
+}
+
// Context represents the run context of a Command. Command implementations
// should interpret file names relative to Dir (see AbsPath below), and print
// output and errors to Stdout and Stderr respectively.
@@ -120,7 +131,7 @@
// return code.
func handleCommandError(c Command, ctx *Context, err error, f *gnuflag.FlagSet) (int, bool) {
if err == gnuflag.ErrHelp {
- ctx.Stderr.Write(c.Info().Help(f))
+ ctx.Stdout.Write(c.Info().Help(f))
return 0, true
}
if err != nil {
@@ -147,7 +158,7 @@
}
if err := c.Run(ctx); err != nil {
if err != ErrSilent {
- log.Printf("%s command failed: %s\n", c.Info().Name, err)
+ log.Errorf("%s command failed: %s\n", c.Info().Name, err)
fmt.Fprintf(ctx.Stderr, "error: %v\n", err)
}
return 1
=== modified file 'cmd/cmd_test.go'
--- cmd/cmd_test.go 2013-02-28 12:27:09 +0000
+++ cmd/cmd_test.go 2013-04-08 08:09:22 +0000
@@ -97,8 +97,8 @@
ctx := testing.Context(c)
result := cmd.Main(&TestCommand{Name: "verb"}, ctx, []string{arg})
c.Assert(result, Equals, 0)
- c.Assert(bufferString(ctx.Stdout), Equals, "")
- c.Assert(bufferString(ctx.Stderr), Equals, fullHelp)
+ c.Assert(bufferString(ctx.Stdout), Equals, fullHelp)
+ c.Assert(bufferString(ctx.Stderr), Equals, "")
}
}
=== modified file 'cmd/juju/addrelation.go'
--- cmd/juju/addrelation.go 2013-02-20 22:11:47 +0000
+++ cmd/juju/addrelation.go 2013-04-08 08:09:22 +0000
@@ -2,14 +2,15 @@
import (
"fmt"
- "launchpad.net/gnuflag"
"launchpad.net/juju-core/cmd"
"launchpad.net/juju-core/juju"
+ "launchpad.net/juju-core/state/api/params"
+ "launchpad.net/juju-core/state/statecmd"
)
-// AddRelationCommand adds relations between service endpoints.
+// AddRelationCommand adds a relation between two service endpoints.
type AddRelationCommand struct {
- EnvName string
+ EnvCommandBase
Endpoints []string
}
@@ -21,10 +22,6 @@
}
}
-func (c *AddRelationCommand) SetFlags(f *gnuflag.FlagSet) {
- addEnvironFlags(&c.EnvName, f)
-}
-
func (c *AddRelationCommand) Init(args []string) error {
if len(args) != 2 {
return fmt.Errorf("a relation must involve two services")
@@ -39,10 +36,9 @@
return err
}
defer conn.Close()
- eps, err := conn.State.InferEndpoints(c.Endpoints)
- if err != nil {
- return err
+ params := params.AddRelation{
+ Endpoints: c.Endpoints,
}
- _, err = conn.State.AddRelation(eps...)
+ _, err = statecmd.AddRelation(conn.State, params)
return err
}
=== modified file 'cmd/juju/addunit.go'
--- cmd/juju/addunit.go 2013-02-20 22:11:47 +0000
+++ cmd/juju/addunit.go 2013-04-08 08:09:22 +0000
@@ -2,15 +2,16 @@
import (
"errors"
-
"launchpad.net/gnuflag"
"launchpad.net/juju-core/cmd"
"launchpad.net/juju-core/juju"
+ "launchpad.net/juju-core/state/api/params"
+ "launchpad.net/juju-core/state/statecmd"
)
// AddUnitCommand is responsible adding additional units to a service.
type AddUnitCommand struct {
- EnvName string
+ EnvCommandBase
ServiceName string
NumUnits int
}
@@ -23,7 +24,7 @@
}
func (c *AddUnitCommand) SetFlags(f *gnuflag.FlagSet) {
- addEnvironFlags(&c.EnvName, f)
+ c.EnvCommandBase.SetFlags(f)
f.IntVar(&c.NumUnits, "n", 1, "number of service units to add")
f.IntVar(&c.NumUnits, "num-units", 1, "")
}
@@ -51,11 +52,10 @@
return err
}
defer conn.Close()
- service, err := conn.State.Service(c.ServiceName)
- if err != nil {
- return err
+
+ params := params.AddServiceUnits{
+ ServiceName: c.ServiceName,
+ NumUnits: c.NumUnits,
}
- _, err = conn.AddUnits(service, c.NumUnits)
- return err
-
+ return statecmd.AddServiceUnits(conn.State, params)
}
=== modified file 'cmd/juju/bootstrap.go'
--- cmd/juju/bootstrap.go 2013-02-24 22:23:26 +0000
+++ cmd/juju/bootstrap.go 2013-04-08 08:09:22 +0000
@@ -3,16 +3,21 @@
import (
"fmt"
"launchpad.net/gnuflag"
+ "launchpad.net/juju-core/charm"
"launchpad.net/juju-core/cmd"
+ "launchpad.net/juju-core/constraints"
"launchpad.net/juju-core/environs"
"os"
+ "strings"
)
// BootstrapCommand is responsible for launching the first machine in a juju
// environment, and setting up everything necessary to continue working.
type BootstrapCommand struct {
- EnvName string
+ EnvCommandBase
+ Constraints constraints.Value
UploadTools bool
+ FakeSeries []string
}
func (c *BootstrapCommand) Info() *cmd.Info {
@@ -23,11 +28,16 @@
}
func (c *BootstrapCommand) SetFlags(f *gnuflag.FlagSet) {
- addEnvironFlags(&c.EnvName, f)
+ c.EnvCommandBase.SetFlags(f)
+ f.Var(constraints.ConstraintsValue{&c.Constraints}, "constraints", "set environment constraints")
f.BoolVar(&c.UploadTools, "upload-tools", false, "upload local version of tools before bootstrapping")
+ f.Var(seriesVar{&c.FakeSeries}, "fake-series", "clone uploaded tools for supplied serieses")
}
func (c *BootstrapCommand) Init(args []string) error {
+ if len(c.FakeSeries) > 0 && !c.UploadTools {
+ return fmt.Errorf("--fake-series requires --upload-tools")
+ }
return cmd.CheckEmpty(args)
}
@@ -36,17 +46,56 @@
// the user is informed how to create one.
func (c *BootstrapCommand) Run(context *cmd.Context) error {
environ, err := environs.NewFromName(c.EnvName)
- if err == nil {
- return environs.Bootstrap(environ, c.UploadTools, nil)
- }
- if !os.IsNotExist(err) {
- return err
- }
- out := context.Stderr
- fmt.Fprintln(out, "No juju environment configuration file exists.")
- fmt.Fprintln(out, "Please create a configuration by running:")
- fmt.Fprintln(out, " juju init -w")
- fmt.Fprintln(out, "then edit the file to configure your juju environment.")
- fmt.Fprintln(out, "You can then re-run bootstrap.")
- return err
+ if err != nil {
+ if os.IsNotExist(err) {
+ out := context.Stderr
+ fmt.Fprintln(out, "No juju environment configuration file exists.")
+ fmt.Fprintln(out, "Please create a configuration by running:")
+ fmt.Fprintln(out, " juju init -w")
+ fmt.Fprintln(out, "then edit the file to configure your juju environment.")
+ fmt.Fprintln(out, "You can then re-run bootstrap.")
+ }
+ return err
+ }
+ // TODO: if in verbose mode, write out to Stdout if a new cert was created.
+ _, err = environs.EnsureCertificate(environ, environs.WriteCertAndKeyToHome)
+ if err != nil {
+ return err
+ }
+
+ if c.UploadTools {
+ tools, err := environs.PutTools(environ.Storage(), nil, c.FakeSeries...)
+ if err != nil {
+ return err
+ }
+ cfg, err := environ.Config().Apply(map[string]interface{}{
+ "agent-version": tools.Number.String(),
+ })
+ if err == nil {
+ err = environ.SetConfig(cfg)
+ }
+ if err != nil {
+ return fmt.Errorf("failed to update environment configuration: %v", err)
+ }
+ }
+ return environs.Bootstrap(environ, c.Constraints)
+}
+
+type seriesVar struct {
+ target *[]string
+}
+
+func (v seriesVar) Set(value string) error {
+ names := strings.Split(value, ",")
+ for _, name := range names {
+ if !charm.ValidSeries.MatchString(name) {
+ return fmt.Errorf("invalid series name %q", name)
+ }
+ }
+ *v.target = names
+ return nil
+}
+
+func (v seriesVar) String() string {
+ return strings.Join(*v.target, ",")
}
=== modified file 'cmd/juju/bootstrap_test.go'
--- cmd/juju/bootstrap_test.go 2013-02-28 12:27:09 +0000
+++ cmd/juju/bootstrap_test.go 2013-04-08 08:09:22 +0000
@@ -2,11 +2,12 @@
import (
"bytes"
- "io/ioutil"
. "launchpad.net/gocheck"
"launchpad.net/juju-core/cmd"
+ "launchpad.net/juju-core/constraints"
"launchpad.net/juju-core/environs"
"launchpad.net/juju-core/environs/agent"
+ "launchpad.net/juju-core/environs/config"
"launchpad.net/juju-core/environs/dummy"
"launchpad.net/juju-core/testing"
"launchpad.net/juju-core/version"
@@ -43,54 +44,117 @@
dummy.Reset()
}
-func (*BootstrapSuite) TestBootstrapCommand(c *C) {
- defer makeFakeHome(c, "brokenenv").restore()
- err := ioutil.WriteFile(homePath(".juju", "environments.yaml"), []byte(envConfig), 0666)
- c.Assert(err, IsNil)
-
- // normal bootstrap
+func (*BootstrapSuite) TestBasic(c *C) {
+ defer testing.MakeFakeHome(c, envConfig).Restore()
opc, errc := runCommand(new(BootstrapCommand))
c.Check(<-errc, IsNil)
- c.Check((<-opc).(dummy.OpBootstrap).Env, Equals, "peckham")
+ opBootstrap := (<-opc).(dummy.OpBootstrap)
+ c.Check(opBootstrap.Env, Equals, "peckham")
+ c.Check(opBootstrap.Constraints, DeepEquals, constraints.Value{})
+}
+
+func (*BootstrapSuite) TestRunGeneratesCertificate(c *C) {
+ defer testing.MakeFakeHome(c, envConfig).Restore()
+ envName := "peckham"
+ _, err := testing.RunCommand(c, new(BootstrapCommand), nil)
+ c.Assert(err, IsNil)
// Check that the CA certificate and key have been automatically generated
// for the environment.
- _, err = os.Stat(homePath(".juju", "peckham-cert.pem"))
- c.Assert(err, IsNil)
- _, err = os.Stat(homePath(".juju", "peckham-private-key.pem"))
- c.Assert(err, IsNil)
-
+ info, err := os.Stat(config.JujuHomePath(envName + "-cert.pem"))
+ c.Assert(err, IsNil)
+ c.Assert(info.Size() > 0, Equals, true)
+ info, err = os.Stat(config.JujuHomePath(envName + "-private-key.pem"))
+ c.Assert(err, IsNil)
+ c.Assert(info.Size() > 0, Equals, true)
+
+ // Check that the environment validates the cert and key.
+ _, err = environs.NewFromName(envName)
+ c.Assert(err, IsNil)
+}
+
+func (*BootstrapSuite) TestConstraints(c *C) {
+ defer testing.MakeFakeHome(c, envConfig, "brokenenv").Restore()
+ scons := " cpu-cores=2 mem=4G"
+ opc, errc := runCommand(new(BootstrapCommand), "--constraints", scons)
+ c.Check(<-errc, IsNil)
+ opBootstrap := (<-opc).(dummy.OpBootstrap)
+ c.Check(opBootstrap.Env, Equals, "peckham")
+ c.Check(opBootstrap.Constraints, DeepEquals, constraints.MustParse(scons))
+}
+
+func (*BootstrapSuite) TestUploadTools(c *C) {
+ defer testing.MakeFakeHome(c, envConfig).Restore()
// bootstrap with tool uploading - checking that a file
// is uploaded should be sufficient, as the detailed semantics
// of UploadTools are tested in environs.
- opc, errc = runCommand(new(BootstrapCommand), "--upload-tools")
+ opc, errc := runCommand(new(BootstrapCommand), "--upload-tools")
c.Check(<-errc, IsNil)
c.Check((<-opc).(dummy.OpPutFile).Env, Equals, "peckham")
- c.Check((<-opc).(dummy.OpBootstrap).Env, Equals, "peckham")
-
+ opBootstrap := (<-opc).(dummy.OpBootstrap)
+ c.Check(opBootstrap.Env, Equals, "peckham")
+ c.Check(opBootstrap.Constraints, DeepEquals, constraints.Value{})
+
+ assertUploadedSomething(c, version.Current)
+}
+
+func assertUploadedSomething(c *C, vers version.Binary) {
+ // Check that some file was uploaded and can be unpacked; detailed
+ // semantics tested elsewhere.
envs, err := environs.ReadEnvirons("")
c.Assert(err, IsNil)
env, err := envs.Open("peckham")
c.Assert(err, IsNil)
-
- tools, err := environs.FindTools(env, version.Current, environs.CompatVersion)
+ tools, err := environs.FindTools(env, vers, environs.CompatVersion)
c.Assert(err, IsNil)
+ c.Assert(tools.Binary, Equals, vers)
resp, err := http.Get(tools.URL)
c.Assert(err, IsNil)
defer resp.Body.Close()
-
err = agent.UnpackTools(c.MkDir(), tools, resp.Body)
c.Assert(err, IsNil)
-
- // bootstrap with broken environment
- opc, errc = runCommand(new(BootstrapCommand), "-e", "brokenenv")
+}
+
+func (*BootstrapSuite) TestUploadToolsFakeSeries(c *C) {
+ defer testing.MakeFakeHome(c, envConfig).Restore()
+ opc, errc := runCommand(new(BootstrapCommand), "--upload-tools", "--fake-series=good,great")
+ c.Check(<-errc, IsNil)
+ c.Check((<-opc).(dummy.OpPutFile).Env, Equals, "peckham")
+ c.Check((<-opc).(dummy.OpPutFile).Env, Equals, "peckham")
+ c.Check((<-opc).(dummy.OpPutFile).Env, Equals, "peckham")
+ c.Check((<-opc).(dummy.OpBootstrap).Env, Equals, "peckham")
+
+ vers := version.Current
+ assertUploadedSomething(c, vers)
+ vers.Series = "good"
+ assertUploadedSomething(c, vers)
+ vers.Series = "great"
+ assertUploadedSomething(c, vers)
+}
+
+func (*BootstrapSuite) TestFakeSeriesBadParams(c *C) {
+ defer testing.MakeFakeHome(c, envConfig).Restore()
+ opc, errc := runCommand(new(BootstrapCommand), "--fake-series=bad1")
+ c.Check(<-errc, ErrorMatches, `invalid value "bad1" for flag --fake-series: invalid series name "bad1"`)
+ c.Check(<-opc, IsNil)
+}
+
+func (*BootstrapSuite) TestFakeSeriesNoUploadTools(c *C) {
+ defer testing.MakeFakeHome(c, envConfig).Restore()
+ opc, errc := runCommand(new(BootstrapCommand), "--fake-series=good,great")
+ c.Check(<-errc, ErrorMatches, `--fake-series requires --upload-tools`)
+ c.Check(<-opc, IsNil)
+}
+
+func (*BootstrapSuite) TestBrokenEnvironment(c *C) {
+ defer testing.MakeFakeHome(c, envConfig).Restore()
+ opc, errc := runCommand(new(BootstrapCommand), "-e", "brokenenv")
c.Check(<-errc, ErrorMatches, "dummy.Bootstrap is broken")
c.Check(<-opc, IsNil)
}
func (*BootstrapSuite) TestMissingEnvironment(c *C) {
- defer makeFakeHome(c, "empty").restore()
- // bootstrap without an environments.yaml
+ defer testing.MakeFakeHomeNoEnvironments(c, "empty").Restore()
ctx := testing.Context(c)
code := cmd.Main(&BootstrapCommand{}, ctx, nil)
c.Check(code, Equals, 1)
=== modified file 'cmd/juju/cmd_test.go'
--- cmd/juju/cmd_test.go 2013-02-28 12:27:09 +0000
+++ cmd/juju/cmd_test.go 2013-04-08 08:09:22 +0000
@@ -1,7 +1,6 @@
package main
import (
- "io/ioutil"
"os"
"reflect"
@@ -14,12 +13,12 @@
type CmdSuite struct {
testing.JujuConnSuite
- home fakeHome
+ home *coretesting.FakeHome
}
var _ = Suite(&CmdSuite{})
-var envConfig = `
+const envConfig = `
default:
peckham
environments:
@@ -41,13 +40,11 @@
func (s *CmdSuite) SetUpTest(c *C) {
s.JujuConnSuite.SetUpTest(c)
- s.home = makeFakeHome(c, "peckham", "walthamstow", "brokenenv")
- err := ioutil.WriteFile(homePath(".juju", "environments.yaml"), []byte(envConfig), 0666)
- c.Assert(err, IsNil)
+ s.home = coretesting.MakeFakeHome(c, envConfig, "peckham", "walthamstow", "brokenenv")
}
func (s *CmdSuite) TearDownTest(c *C) {
- s.home.restore()
+ s.home.Restore()
s.JujuConnSuite.TearDownTest(c)
}
@@ -102,6 +99,14 @@
testInit(c, com, append(args, "--environment", "walthamstow"), "")
assertConnName(c, com, "walthamstow")
+ // JUJU_ENV is the final place the environment can be overriden
+ com, args = cmdFunc()
+ oldenv := os.Getenv("JUJU_ENV")
+ os.Setenv("JUJU_ENV", "walthamstow")
+ testInit(c, com, args, "")
+ os.Setenv("JUJU_ENV", oldenv)
+ assertConnName(c, com, "walthamstow")
+
com, args = cmdFunc()
testInit(c, com, append(args, "hotdog"), "unrecognized args.*")
}
@@ -217,12 +222,6 @@
_, err = initDeployCommand()
c.Assert(err, ErrorMatches, "no charm specified")
- // bad unit count
- _, err = initDeployCommand("charm-name", "--num-units", "0")
- c.Assert(err, ErrorMatches, "must deploy at least one unit")
- _, err = initDeployCommand("charm-name", "-n", "0")
- c.Assert(err, ErrorMatches, "must deploy at least one unit")
-
// environment tested elsewhere
}
@@ -237,10 +236,10 @@
c.Assert(err, ErrorMatches, "no service specified")
// bad unit count
- _, err = initAddUnitCommand("service-name", "--num-units", "0")
- c.Assert(err, ErrorMatches, "must add at least one unit")
- _, err = initAddUnitCommand("service-name", "-n", "0")
- c.Assert(err, ErrorMatches, "must add at least one unit")
+ _, err = initDeployCommand("charm-name", "--num-units", "0")
+ c.Assert(err, ErrorMatches, "must deploy at least one unit")
+ _, err = initDeployCommand("charm-name", "-n", "0")
+ c.Assert(err, ErrorMatches, "must deploy at least one unit")
// environment tested elsewhere
}
=== modified file 'cmd/juju/constraints.go'
--- cmd/juju/constraints.go 2013-02-24 22:23:26 +0000
+++ cmd/juju/constraints.go 2013-04-08 08:09:22 +0000
@@ -4,13 +4,16 @@
"fmt"
"launchpad.net/gnuflag"
"launchpad.net/juju-core/cmd"
+ "launchpad.net/juju-core/constraints"
"launchpad.net/juju-core/juju"
"launchpad.net/juju-core/state"
+ "launchpad.net/juju-core/state/api/params"
+ "launchpad.net/juju-core/state/statecmd"
)
// GetConstraintsCommand shows the constraints for a service or environment.
type GetConstraintsCommand struct {
- EnvName string
+ EnvCommandBase
ServiceName string
out cmd.Output
}
@@ -24,11 +27,11 @@
}
func formatConstraints(value interface{}) ([]byte, error) {
- return []byte(value.(state.Constraints).String()), nil
+ return []byte(value.(constraints.Value).String()), nil
}
func (c *GetConstraintsCommand) SetFlags(f *gnuflag.FlagSet) {
- addEnvironFlags(&c.EnvName, f)
+ c.EnvCommandBase.SetFlags(f)
c.out.AddFlags(f, "constraints", map[string]cmd.Formatter{
"constraints": formatConstraints,
"yaml": cmd.FormatYaml,
@@ -46,21 +49,23 @@
return cmd.CheckEmpty(args)
}
-func (c *GetConstraintsCommand) Run(ctx *cmd.Context) (err error) {
+func (c *GetConstraintsCommand) Run(ctx *cmd.Context) error {
conn, err := juju.NewConnFromName(c.EnvName)
if err != nil {
return err
}
defer conn.Close()
- var cons state.Constraints
- if c.ServiceName == "" {
+
+ var cons constraints.Value
+ if c.ServiceName != "" {
+ args := params.GetServiceConstraints{
+ ServiceName: c.ServiceName,
+ }
+ var results params.GetServiceConstraintsResults
+ results, err = statecmd.GetServiceConstraints(conn.State, args)
+ cons = results.Constraints
+ } else {
cons, err = conn.State.EnvironConstraints()
- } else {
- var svc *state.Service
- if svc, err = conn.State.Service(c.ServiceName); err != nil {
- return err
- }
- cons, err = svc.Constraints()
}
if err != nil {
return err
@@ -70,9 +75,9 @@
// SetConstraintsCommand shows the constraints for a service or environment.
type SetConstraintsCommand struct {
- EnvName string
+ EnvCommandBase
ServiceName string
- Constraints state.Constraints
+ Constraints constraints.Value
}
func (c *SetConstraintsCommand) Info() *cmd.Info {
@@ -84,7 +89,7 @@
}
func (c *SetConstraintsCommand) SetFlags(f *gnuflag.FlagSet) {
- addEnvironFlags(&c.EnvName, f)
+ c.EnvCommandBase.SetFlags(f)
f.StringVar(&c.ServiceName, "s", "", "set service constraints")
f.StringVar(&c.ServiceName, "service", "", "")
}
@@ -93,7 +98,7 @@
if c.ServiceName != "" && !state.IsServiceName(c.ServiceName) {
return fmt.Errorf("invalid service name %q", c.ServiceName)
}
- c.Constraints, err = state.ParseConstraints(args...)
+ c.Constraints, err = constraints.Parse(args...)
return err
}
@@ -106,9 +111,9 @@
if c.ServiceName == "" {
return conn.State.SetEnvironConstraints(c.Constraints)
}
- var svc *state.Service
- if svc, err = conn.State.Service(c.ServiceName); err != nil {
- return err
+ params := params.SetServiceConstraints{
+ ServiceName: c.ServiceName,
+ Constraints: c.Constraints,
}
- return svc.SetConstraints(c.Constraints)
+ return statecmd.SetServiceConstraints(conn.State, params)
}
=== modified file 'cmd/juju/constraints_test.go'
--- cmd/juju/constraints_test.go 2013-02-28 12:27:09 +0000
+++ cmd/juju/constraints_test.go 2013-04-08 08:09:22 +0000
@@ -4,8 +4,8 @@
"bytes"
. "launchpad.net/gocheck"
"launchpad.net/juju-core/cmd"
+ "launchpad.net/juju-core/constraints"
"launchpad.net/juju-core/juju/testing"
- "launchpad.net/juju-core/state"
coretesting "launchpad.net/juju-core/testing"
)
@@ -40,7 +40,7 @@
assertSet(c, "mem=4G", "cpu-power=250")
cons, err := s.State.EnvironConstraints()
c.Assert(err, IsNil)
- c.Assert(cons, DeepEquals, state.Constraints{
+ c.Assert(cons, DeepEquals, constraints.Value{
CpuPower: uint64p(250),
Mem: uint64p(4096),
})
@@ -49,7 +49,7 @@
assertSet(c)
cons, err = s.State.EnvironConstraints()
c.Assert(err, IsNil)
- c.Assert(cons, DeepEquals, state.Constraints{})
+ c.Assert(cons, DeepEquals, constraints.Value{})
}
func (s *ConstraintsCommandsSuite) TestSetService(c *C) {
@@ -60,7 +60,7 @@
assertSet(c, "-s", "svc", "mem=4G", "cpu-power=250")
cons, err := svc.Constraints()
c.Assert(err, IsNil)
- c.Assert(cons, DeepEquals, state.Constraints{
+ c.Assert(cons, DeepEquals, constraints.Value{
CpuPower: uint64p(250),
Mem: uint64p(4096),
})
@@ -69,7 +69,7 @@
assertSet(c, "-s", "svc")
cons, err = svc.Constraints()
c.Assert(err, IsNil)
- c.Assert(cons, DeepEquals, state.Constraints{})
+ c.Assert(cons, DeepEquals, constraints.Value{})
}
func assertSetError(c *C, code int, stderr string, args ...string) {
@@ -98,7 +98,7 @@
}
func (s *ConstraintsCommandsSuite) TestGetEnvironValues(c *C) {
- cons := state.Constraints{CpuCores: uint64p(64)}
+ cons := constraints.Value{CpuCores: uint64p(64)}
err := s.State.SetEnvironConstraints(cons)
c.Assert(err, IsNil)
assertGet(c, "cpu-cores=64\n")
@@ -113,13 +113,13 @@
func (s *ConstraintsCommandsSuite) TestGetServiceValues(c *C) {
svc, err := s.State.AddService("svc", s.AddTestingCharm(c, "dummy"))
c.Assert(err, IsNil)
- err = svc.SetConstraints(state.Constraints{CpuCores: uint64p(64)})
+ err = svc.SetConstraints(constraints.Value{CpuCores: uint64p(64)})
c.Assert(err, IsNil)
assertGet(c, "cpu-cores=64\n", "svc")
}
func (s *ConstraintsCommandsSuite) TestGetFormats(c *C) {
- cons := state.Constraints{CpuCores: uint64p(64), CpuPower: uint64p(0)}
+ cons := constraints.Value{CpuCores: uint64p(64), CpuPower: uint64p(0)}
err := s.State.SetEnvironConstraints(cons)
c.Assert(err, IsNil)
assertGet(c, "cpu-cores=64 cpu-power=\n", "--format", "constraints")
=== added file 'cmd/juju/debuglog.go'
--- cmd/juju/debuglog.go 1970-01-01 00:00:00 +0000
+++ cmd/juju/debuglog.go 2013-04-08 08:09:22 +0000
@@ -0,0 +1,42 @@
+package main
+
+import (
+ "launchpad.net/gnuflag"
+ "launchpad.net/juju-core/cmd"
+)
+
+type DebugLogCommand struct {
+ // The debug log command simply invokes juju ssh with the required arguments.
+ sshCmd cmd.Command
+}
+
+const debuglogDoc = `
+Launch an ssh shell on the state server machine and tail the consolidated log file.
+The consolidated log file contains log messages from all nodes in the environment.
+`
+
+func (c *DebugLogCommand) Info() *cmd.Info {
+ return &cmd.Info{
+ Name: "debug-log",
+ Args: "[<ssh args>...]",
+ Purpose: "display the consolidated log file",
+ Doc: debuglogDoc,
+ }
+}
+
+func (c *DebugLogCommand) SetFlags(f *gnuflag.FlagSet) {
+ c.sshCmd.SetFlags(f)
+}
+
+func (c *DebugLogCommand) Init(args []string) error {
+ args = append([]string{"0"}, args...)
+ args = append(args, "tail -f /var/log/juju/all-machines.log")
+ return c.sshCmd.Init(args)
+}
+
+// Run uses "juju ssh" to log into the state server node
+// and tails the consolidated log file which captures log
+// messages from all nodes.
+func (c *DebugLogCommand) Run(ctx *cmd.Context) error {
+ return c.sshCmd.Run(ctx)
+}
=== added file 'cmd/juju/debuglog_test.go'
--- cmd/juju/debuglog_test.go 1970-01-01 00:00:00 +0000
+++ cmd/juju/debuglog_test.go 2013-04-08 08:09:22 +0000
@@ -0,0 +1,41 @@
+package main
+
+import (
+ . "launchpad.net/gocheck"
+ "launchpad.net/juju-core/cmd"
+ "launchpad.net/juju-core/testing"
+)
+
+type DebugLogSuite struct {
+}
+
+var _ = Suite(&DebugLogSuite{})
+
+func runDebugLog(c *C, args ...string) (*DebugLogCommand, error) {
+ cmd := &DebugLogCommand{
+ sshCmd: &dummySSHCommand{},
+ }
+ _, err := testing.RunCommand(c, cmd, args)
+ return cmd, err
+}
+
+type dummySSHCommand struct {
+ SSHCommand
+ runCalled bool
+}
+
+func (c *dummySSHCommand) Run(ctx *cmd.Context) error {
+ c.runCalled = true
+ return nil
+}
+
+// debug-log is implemented by invoking juju ssh with the correct arguments.
+// This test checks for the expected invocation.
+func (s *DebugLogSuite) TestDebugLogInvokesSSHCommand(c *C) {
+ debugLogCmd, err := runDebugLog(c)
+ c.Assert(err, IsNil)
+ debugCmd := debugLogCmd.sshCmd.(*dummySSHCommand)
+ c.Assert(debugCmd.runCalled, Equals, true)
+ c.Assert(debugCmd.Target, Equals, "0")
+ c.Assert([]string{"tail -f /var/log/juju/all-machines.log"}, DeepEquals, debugCmd.Args)
+}
=== modified file 'cmd/juju/deploy.go'
--- cmd/juju/deploy.go 2013-02-20 22:11:47 +0000
+++ cmd/juju/deploy.go 2013-04-08 08:09:22 +0000
@@ -3,19 +3,22 @@
import (
"errors"
"fmt"
+ "io/ioutil"
"launchpad.net/gnuflag"
"launchpad.net/juju-core/charm"
"launchpad.net/juju-core/cmd"
+ "launchpad.net/juju-core/constraints"
"launchpad.net/juju-core/juju"
"launchpad.net/juju-core/state"
"os"
)
type DeployCommand struct {
- EnvName string
+ EnvCommandBase
CharmName string
ServiceName string
Config cmd.FileVar
+ Constraints constraints.Value
NumUnits int // defaults to 1
BumpRevision bool
RepoPath string // defaults to JUJU_REPOSITORY
@@ -52,12 +55,13 @@
}
func (c *DeployCommand) SetFlags(f *gnuflag.FlagSet) {
- addEnvironFlags(&c.EnvName, f)
+ c.EnvCommandBase.SetFlags(f)
f.IntVar(&c.NumUnits, "n", 1, "number of service units to deploy for principal charms")
f.IntVar(&c.NumUnits, "num-units", 1, "")
f.BoolVar(&c.BumpRevision, "u", false, "increment local charm directory revision")
f.BoolVar(&c.BumpRevision, "upgrade", false, "")
f.Var(&c.Config, "config", "path to yaml-formatted service config")
+ f.Var(constraints.ConstraintsValue{&c.Constraints}, "constraints", "set service constraints")
f.StringVar(&c.RepoPath, "repository", os.Getenv("JUJU_REPOSITORY"), "local charm repository")
}
@@ -105,25 +109,35 @@
if err != nil {
return err
}
- ch, err := conn.PutCharm(curl, repo, c.BumpRevision)
- if err != nil {
- return err
- }
+ var configYAML []byte
if c.Config.Path != "" {
- // TODO many dependencies :(
- return errors.New("state.Service.SetConfig not implemented (format 2...)")
- }
- svcName := c.ServiceName
- if svcName == "" {
- svcName = curl.Name
- }
- svc, err := conn.State.AddService(svcName, ch)
+ configYAML, err = ioutil.ReadFile(c.Config.Path)
+ if err != nil {
+ return err
+ }
+ }
+ charm, err := conn.PutCharm(curl, repo, c.BumpRevision)
if err != nil {
return err
}
- if ch.Meta().Subordinate {
- return nil
- }
- _, err = conn.AddUnits(svc, c.NumUnits)
+ if charm.Meta().Subordinate {
+ empty := constraints.Value{}
+ if c.Constraints != empty {
+ return state.ErrSubordinateConstraints
+ }
+ }
+ serviceName := c.ServiceName
+ if serviceName == "" {
+ serviceName = curl.Name
+ }
+ args := juju.DeployServiceParams{
+ Charm: charm,
+ ServiceName: serviceName,
+ NumUnits: c.NumUnits,
+ // BUG(lp:1162122): --config has no tests.
+ ConfigYAML: string(configYAML),
+ Constraints: c.Constraints,
+ }
+ _, err = conn.DeployService(args)
return err
}
=== modified file 'cmd/juju/deploy_test.go'
--- cmd/juju/deploy_test.go 2013-02-28 12:27:09 +0000
+++ cmd/juju/deploy_test.go 2013-04-08 08:09:22 +0000
@@ -6,6 +6,7 @@
"io/ioutil"
. "launchpad.net/gocheck"
"launchpad.net/juju-core/charm"
+ "launchpad.net/juju-core/constraints"
"launchpad.net/juju-core/juju/testing"
"launchpad.net/juju-core/state"
coretesting "launchpad.net/juju-core/testing"
@@ -126,6 +127,9 @@
}, {
args: []string{"craziness", "burble1", "-n", "0"},
err: `must deploy at least one unit`,
+ }, {
+ args: []string{"craziness", "burble1", "--constraints", "gibber=plop"},
+ err: `invalid value "gibber=plop" for flag --constraints: unknown constraint "gibber"`,
},
}
@@ -168,7 +172,7 @@
func (s *DeploySuite) TestCannotUpgradeCharmBundle(c *C) {
coretesting.Charms.BundlePath(s.seriesPath, "dummy")
err := runDeploy(c, "local:dummy", "-u")
- c.Assert(err, ErrorMatches, `cannot increment version of charm "local:precise/dummy-1": not a directory`)
+ c.Assert(err, ErrorMatches, `cannot increment revision of charm "local:precise/dummy-1": not a directory`)
// Verify state not touched...
curl := charm.MustParseURL("local:precise/dummy-1")
_, err = s.State.Charm(curl)
@@ -186,9 +190,9 @@
rel := rels[0]
ep, err := rel.Endpoint("riak")
c.Assert(err, IsNil)
- c.Assert(ep.RelationName, Equals, "ring")
- c.Assert(ep.RelationRole, Equals, state.RolePeer)
- c.Assert(ep.RelationScope, Equals, charm.ScopeGlobal)
+ c.Assert(ep.Name, Equals, "ring")
+ c.Assert(ep.Role, Equals, charm.RolePeer)
+ c.Assert(ep.Scope, Equals, charm.ScopeGlobal)
}
func (s *DeploySuite) TestNumUnits(c *C) {
@@ -206,3 +210,20 @@
curl := charm.MustParseURL("local:precise/logging-1")
s.assertService(c, "logging", curl, 0, 0)
}
+
+func (s *DeploySuite) TestConstraints(c *C) {
+ coretesting.Charms.BundlePath(s.seriesPath, "dummy")
+ err := runDeploy(c, "local:dummy", "--constraints", "mem=2G cpu-cores=2")
+ c.Assert(err, IsNil)
+ curl := charm.MustParseURL("local:precise/dummy-1")
+ service, _ := s.assertService(c, "dummy", curl, 1, 0)
+ cons, err := service.Constraints()
+ c.Assert(err, IsNil)
+ c.Assert(cons, DeepEquals, constraints.MustParse("mem=2G cpu-cores=2"))
+}
+
+func (s *DeploySuite) TestSubordinateConstraints(c *C) {
+ coretesting.Charms.BundlePath(s.seriesPath, "logging")
+ err := runDeploy(c, "local:logging", "--constraints", "mem=1G")
+ c.Assert(err, Equals, state.ErrSubordinateConstraints)
+}
=== modified file 'cmd/juju/destroyenvironment.go'
--- cmd/juju/destroyenvironment.go 2013-02-20 22:11:47 +0000
+++ cmd/juju/destroyenvironment.go 2013-04-08 08:09:22 +0000
@@ -1,14 +1,13 @@
package main
import (
- "launchpad.net/gnuflag"
"launchpad.net/juju-core/cmd"
"launchpad.net/juju-core/environs"
)
// DestroyEnvironmentCommand destroys an environment.
type DestroyEnvironmentCommand struct {
- EnvName string
+ EnvCommandBase
}
func (c *DestroyEnvironmentCommand) Info() *cmd.Info {
@@ -18,14 +17,6 @@
}
}
-func (c *DestroyEnvironmentCommand) SetFlags(f *gnuflag.FlagSet) {
- addEnvironFlags(&c.EnvName, f)
-}
-
-func (c *DestroyEnvironmentCommand) Init(args []string) error {
- return cmd.CheckEmpty(args)
-}
-
func (c *DestroyEnvironmentCommand) Run(_ *cmd.Context) error {
environ, err := environs.NewFromName(c.EnvName)
if err != nil {
=== modified file 'cmd/juju/destroymachine.go'
--- cmd/juju/destroymachine.go 2013-02-24 22:23:26 +0000
+++ cmd/juju/destroymachine.go 2013-04-08 08:09:22 +0000
@@ -2,7 +2,6 @@
import (
"fmt"
- "launchpad.net/gnuflag"
"launchpad.net/juju-core/cmd"
"launchpad.net/juju-core/juju"
"launchpad.net/juju-core/state"
@@ -10,7 +9,7 @@
// DestroyMachineCommand causes an existing machine to be destroyed.
type DestroyMachineCommand struct {
- EnvName string
+ EnvCommandBase
MachineIds []string
}
@@ -24,10 +23,6 @@
}
}
-func (c *DestroyMachineCommand) SetFlags(f *gnuflag.FlagSet) {
- addEnvironFlags(&c.EnvName, f)
-}
-
func (c *DestroyMachineCommand) Init(args []string) error {
if len(args) == 0 {
return fmt.Errorf("no machines specified")
@@ -47,5 +42,5 @@
return err
}
defer conn.Close()
- return conn.DestroyMachines(c.MachineIds...)
+ return conn.State.DestroyMachines(c.MachineIds...)
}
=== modified file 'cmd/juju/destroyrelation.go'
--- cmd/juju/destroyrelation.go 2013-02-20 22:11:47 +0000
+++ cmd/juju/destroyrelation.go 2013-04-08 08:09:22 +0000
@@ -2,14 +2,15 @@
import (
"fmt"
- "launchpad.net/gnuflag"
"launchpad.net/juju-core/cmd"
"launchpad.net/juju-core/juju"
+ "launchpad.net/juju-core/state/api/params"
+ "launchpad.net/juju-core/state/statecmd"
)
// DestroyRelationCommand causes an existing service relation to be shut down.
type DestroyRelationCommand struct {
- EnvName string
+ EnvCommandBase
Endpoints []string
}
@@ -22,10 +23,6 @@
}
}
-func (c *DestroyRelationCommand) SetFlags(f *gnuflag.FlagSet) {
- addEnvironFlags(&c.EnvName, f)
-}
-
func (c *DestroyRelationCommand) Init(args []string) error {
if len(args) != 2 {
return fmt.Errorf("a relation must involve two services")
@@ -40,13 +37,9 @@
return err
}
defer conn.Close()
- eps, err := conn.State.InferEndpoints(c.Endpoints)
- if err != nil {
- return err
- }
- rel, err := conn.State.EndpointsRelation(eps...)
- if err != nil {
- return err
- }
- return rel.Destroy()
+
+ params := params.DestroyRelation{
+ Endpoints: c.Endpoints,
+ }
+ return statecmd.DestroyRelation(conn.State, params)
}
=== modified file 'cmd/juju/destroyservice.go'
--- cmd/juju/destroyservice.go 2013-02-20 22:11:47 +0000
+++ cmd/juju/destroyservice.go 2013-04-08 08:09:22 +0000
@@ -2,15 +2,16 @@
import (
"fmt"
- "launchpad.net/gnuflag"
"launchpad.net/juju-core/cmd"
"launchpad.net/juju-core/juju"
"launchpad.net/juju-core/state"
+ "launchpad.net/juju-core/state/api/params"
+ "launchpad.net/juju-core/state/statecmd"
)
// DestroyServiceCommand causes an existing service to be destroyed.
type DestroyServiceCommand struct {
- EnvName string
+ EnvCommandBase
ServiceName string
}
@@ -23,10 +24,6 @@
}
}
-func (c *DestroyServiceCommand) SetFlags(f *gnuflag.FlagSet) {
- addEnvironFlags(&c.EnvName, f)
-}
-
func (c *DestroyServiceCommand) Init(args []string) error {
if len(args) == 0 {
return fmt.Errorf("no service specified")
@@ -44,9 +41,9 @@
return err
}
defer conn.Close()
- svc, err := conn.State.Service(c.ServiceName)
- if err != nil {
- return err
+
+ params := params.ServiceDestroy{
+ ServiceName: c.ServiceName,
}
- return svc.Destroy()
+ return statecmd.ServiceDestroy(conn.State, params)
}
=== modified file 'cmd/juju/destroyunit.go'
--- cmd/juju/destroyunit.go 2013-02-20 22:11:47 +0000
+++ cmd/juju/destroyunit.go 2013-04-08 08:09:22 +0000
@@ -3,16 +3,16 @@
import (
"errors"
"fmt"
-
- "launchpad.net/gnuflag"
"launchpad.net/juju-core/cmd"
"launchpad.net/juju-core/juju"
"launchpad.net/juju-core/state"
+ "launchpad.net/juju-core/state/api/params"
+ "launchpad.net/juju-core/state/statecmd"
)
// DestroyUnitCommand is responsible for destroying service units.
type DestroyUnitCommand struct {
- EnvName string
+ EnvCommandBase
UnitNames []string
}
@@ -25,10 +25,6 @@
}
}
-func (c *DestroyUnitCommand) SetFlags(f *gnuflag.FlagSet) {
- addEnvironFlags(&c.EnvName, f)
-}
-
func (c *DestroyUnitCommand) Init(args []string) error {
c.UnitNames = args
if len(c.UnitNames) == 0 {
@@ -42,13 +38,16 @@
return nil
}
-// Run connects to the environment specified on the command line
-// and calls conn.DestroyUnits.
+// Run connects to the environment specified on the command line and destroys
+// units therein.
func (c *DestroyUnitCommand) Run(_ *cmd.Context) (err error) {
conn, err := juju.NewConnFromName(c.EnvName)
if err != nil {
return err
}
defer conn.Close()
- return conn.DestroyUnits(c.UnitNames...)
+ params := params.DestroyServiceUnits{
+ UnitNames: c.UnitNames,
+ }
+ return statecmd.DestroyServiceUnits(conn.State, params)
}
=== added file 'cmd/juju/environmentcommand.go'
--- cmd/juju/environmentcommand.go 1970-01-01 00:00:00 +0000
+++ cmd/juju/environmentcommand.go 2013-04-08 08:09:22 +0000
@@ -0,0 +1,20 @@
+package main
+
+import (
+ "launchpad.net/gnuflag"
+ "launchpad.net/juju-core/cmd"
+ "os"
+)
+
+// The purpose of EnvCommandBase is to provide a default member and flag
+// setting for commands that deal across different environments.
+type EnvCommandBase struct {
+ cmd.CommandBase
+ EnvName string
+}
+
+func (c *EnvCommandBase) SetFlags(f *gnuflag.FlagSet) {
+ defaultEnv := os.Getenv("JUJU_ENV")
+ f.StringVar(&c.EnvName, "e", defaultEnv, "juju environment to operate in")
+ f.StringVar(&c.EnvName, "environment", defaultEnv, "")
+}
=== modified file 'cmd/juju/expose.go'
--- cmd/juju/expose.go 2013-02-27 14:47:18 +0000
+++ cmd/juju/expose.go 2013-04-08 08:09:22 +0000
@@ -2,16 +2,15 @@
import (
"errors"
-
- "launchpad.net/gnuflag"
"launchpad.net/juju-core/cmd"
"launchpad.net/juju-core/juju"
+ "launchpad.net/juju-core/state/api/params"
"launchpad.net/juju-core/state/statecmd"
)
// ExposeCommand is responsible exposing services.
type ExposeCommand struct {
- EnvName string
+ EnvCommandBase
ServiceName string
}
@@ -23,10 +22,6 @@
}
}
-func (c *ExposeCommand) SetFlags(f *gnuflag.FlagSet) {
- addEnvironFlags(&c.EnvName, f)
-}
-
func (c *ExposeCommand) Init(args []string) error {
if len(args) == 0 {
return errors.New("no service name specified")
@@ -44,7 +39,7 @@
}
defer conn.Close()
- params := statecmd.ServiceExposeParams{
+ params := params.ServiceExpose{
ServiceName: c.ServiceName,
}
return statecmd.ServiceExpose(conn.State, params)
=== modified file 'cmd/juju/get.go'
--- cmd/juju/get.go 2013-02-24 21:55:08 +0000
+++ cmd/juju/get.go 2013-04-08 08:09:22 +0000
@@ -6,12 +6,13 @@
"launchpad.net/gnuflag"
"launchpad.net/juju-core/cmd"
"launchpad.net/juju-core/juju"
+ "launchpad.net/juju-core/state/api/params"
"launchpad.net/juju-core/state/statecmd"
)
// GetCommand retrieves the configuration of a service.
type GetCommand struct {
- EnvName string
+ EnvCommandBase
ServiceName string
out cmd.Output
}
@@ -25,7 +26,7 @@
}
func (c *GetCommand) SetFlags(f *gnuflag.FlagSet) {
- addEnvironFlags(&c.EnvName, f)
+ c.EnvCommandBase.SetFlags(f)
// TODO(dfc) add json formatting ?
c.out.AddFlags(f, "yaml", map[string]cmd.Formatter{
"yaml": cmd.FormatYaml,
@@ -50,7 +51,7 @@
}
defer conn.Close()
- params := statecmd.ServiceGetParams{
+ params := params.ServiceGet{
ServiceName: c.ServiceName,
}
=== added file 'cmd/juju/help_topics.go'
--- cmd/juju/help_topics.go 1970-01-01 00:00:00 +0000
+++ cmd/juju/help_topics.go 2013-04-08 08:09:22 +0000
@@ -0,0 +1,21 @@
+package main
+
+const helpBasics = `
+Juju -- devops distilled
+https://juju.ubuntu.com/
+
+Juju provides easy, intelligent service orchestration on top of environments
+such as OpenStack, Amazon AWS, or bare metal.
+
+Basic commands:
+ juju init generate boilerplate configuration for juju environments
+ juju bootstrap start up an environment from scratch
+
+ juju deploy deploy a new service
+ juju add-relation add a relation between two services
+ juju expose expose a service
+
+ juju help bootstrap more help on e.g. bootstrap command
+ juju help commands list all commands
+ juju help topics list all help topics
+`
=== modified file 'cmd/juju/init.go'
--- cmd/juju/init.go 2013-02-24 22:26:04 +0000
+++ cmd/juju/init.go 2013-04-08 08:09:22 +0000
@@ -10,6 +10,7 @@
// InitCommand is used to write out a boilerplate environments.yaml file.
type InitCommand struct {
+ cmd.CommandBase
WriteFile bool
}
@@ -25,10 +26,6 @@
f.BoolVar(&c.WriteFile, "w", false, "write to environments.yaml file if it doesn't already exist")
}
-func (c *InitCommand) Init(args []string) error {
- return cmd.CheckEmpty(args)
-}
-
// Run checks to see if there is already an environments.yaml file. In one does not exist already,
// a boilerplate version is created so that the user can edit it to get started.
func (c *InitCommand) Run(context *cmd.Context) error {
=== modified file 'cmd/juju/init_test.go'
--- cmd/juju/init_test.go 2013-02-28 12:27:09 +0000
+++ cmd/juju/init_test.go 2013-04-08 08:09:22 +0000
@@ -5,7 +5,6 @@
"io/ioutil"
. "launchpad.net/gocheck"
"launchpad.net/juju-core/cmd"
- "launchpad.net/juju-core/environs"
"launchpad.net/juju-core/testing"
"strings"
)
@@ -16,7 +15,7 @@
var _ = Suite(&InitSuite{})
func (*InitSuite) TestBoilerPlateEnvironment(c *C) {
- defer makeFakeHome(c, "empty").restore()
+ defer testing.MakeEmptyFakeHome(c).Restore()
// run without an environments.yaml
ctx := testing.Context(c)
code := cmd.Main(&InitCommand{}, ctx, []string{"-w"})
@@ -24,25 +23,23 @@
outStr := ctx.Stdout.(*bytes.Buffer).String()
strippedOut := strings.Replace(outStr, "\n", "", -1)
c.Check(strippedOut, Matches, ".*A boilerplate environment configuration file has been written.*")
- environpath := homePath(".juju", "environments.yaml")
+ environpath := testing.HomePath(".juju", "environments.yaml")
data, err := ioutil.ReadFile(environpath)
c.Assert(err, IsNil)
strippedData := strings.Replace(string(data), "\n", "", -1)
c.Assert(strippedData, Matches, ".*## This is the Juju config file, which you can use.*")
}
+const existingEnv = `
+environments:
+ test:
+ type: dummy
+ state-server: false
+ authorized-keys: i-am-a-key
+`
+
func (*InitSuite) TestExistingEnvironmentNotOverwritten(c *C) {
- defer makeFakeHome(c, "existing").restore()
- env := `
-environments:
- test:
- type: dummy
- state-server: false
- authorized-keys: i-am-a-key
-`
- environpath := homePath(".juju", "environments.yaml")
- _, err := environs.WriteEnvirons(environpath, env)
- c.Assert(err, IsNil)
+ defer testing.MakeFakeHome(c, existingEnv, "existing").Restore()
ctx := testing.Context(c)
code := cmd.Main(&InitCommand{}, ctx, []string{"-w"})
@@ -50,25 +47,16 @@
errOut := ctx.Stdout.(*bytes.Buffer).String()
strippedOut := strings.Replace(errOut, "\n", "", -1)
c.Check(strippedOut, Matches, ".*A juju environment configuration already exists.*")
+ environpath := testing.HomePath(".juju", "environments.yaml")
data, err := ioutil.ReadFile(environpath)
c.Assert(err, IsNil)
- c.Assert(string(data), Equals, env)
+ c.Assert(string(data), Equals, existingEnv)
}
// Without the write (-w) option, any existing environmens.yaml file is preserved and the boilerplate is
// written to stdout.
func (*InitSuite) TestPrintBoilerplate(c *C) {
- defer makeFakeHome(c, "existing").restore()
- env := `
-environments:
- test:
- type: dummy
- state-server: false
- authorized-keys: i-am-a-key
-`
- environpath := homePath(".juju", "environments.yaml")
- _, err := environs.WriteEnvirons(environpath, env)
- c.Assert(err, IsNil)
+ defer testing.MakeFakeHome(c, existingEnv, "existing").Restore()
ctx := testing.Context(c)
code := cmd.Main(&InitCommand{}, ctx, nil)
@@ -76,7 +64,8 @@
errOut := ctx.Stdout.(*bytes.Buffer).String()
strippedOut := strings.Replace(errOut, "\n", "", -1)
c.Check(strippedOut, Matches, ".*## This is the Juju config file, which you can use.*")
+ environpath := testing.HomePath(".juju", "environments.yaml")
data, err := ioutil.ReadFile(environpath)
c.Assert(err, IsNil)
- c.Assert(string(data), Equals, env)
+ c.Assert(string(data), Equals, existingEnv)
}
=== modified file 'cmd/juju/main.go'
--- cmd/juju/main.go 2013-03-11 14:00:49 +0000
+++ cmd/juju/main.go 2013-04-08 08:09:22 +0000
@@ -1,9 +1,11 @@
package main
import (
- "launchpad.net/gnuflag"
+ "fmt"
"launchpad.net/juju-core/cmd"
+ "launchpad.net/juju-core/environs/config"
"os"
+ "path/filepath"
)
// When we import an environment provider implementation
@@ -22,11 +24,33 @@
https://juju.ubuntu.com/
`
+// checkJujuHome retrieves $JUJU_HOME or $HOME to set the juju home.
+// In case both variables aren't set the command will exit with an
+// error.
+func checkJujuHome() {
+ jujuHome := os.Getenv("JUJU_HOME")
+ if jujuHome == "" {
+ home := os.Getenv("HOME")
+ if home == "" {
+ fmt.Fprintf(os.Stderr, "command failed: cannot determine juju home, neither $JUJU_HOME nor $HOME are set")
+ os.Exit(1)
+ }
+ jujuHome = filepath.Join(home, ".juju")
+ }
+ config.SetJujuHome(jujuHome)
+}
+
// Main registers subcommands for the juju executable, and hands over control
// to the cmd package. This function is not redundant with main, because it
// provides an entry point for testing with arbitrary command line arguments.
func Main(args []string) {
- juju := &cmd.SuperCommand{Name: "juju", Doc: jujuDoc, Log: &cmd.Log{}}
+ checkJujuHome()
+ juju := cmd.NewSuperCommand(cmd.SuperCommandParams{
+ Name: "juju",
+ Doc: jujuDoc,
+ Log: &cmd.Log{},
+ })
+ juju.AddHelpTopic("basics", "Basic commands", helpBasics)
// Register creation commands.
juju.Register(&BootstrapCommand{})
@@ -46,6 +70,7 @@
juju.Register(&SCPCommand{})
juju.Register(&SSHCommand{})
juju.Register(&ResolvedCommand{})
+ juju.Register(&DebugLogCommand{sshCmd: &SSHCommand{}})
// Register configuration commands.
juju.Register(&InitCommand{})
@@ -54,8 +79,13 @@
juju.Register(&GetConstraintsCommand{})
juju.Register(&SetConstraintsCommand{})
juju.Register(&ExposeCommand{})
+ juju.Register(&SyncToolsCommand{})
juju.Register(&UnexposeCommand{})
juju.Register(&UpgradeJujuCommand{})
+ juju.Register(&UpgradeCharmCommand{})
+
+ // register common commands
+ juju.Register(&cmd.VersionCommand{})
os.Exit(cmd.Main(juju, cmd.DefaultContext(), args[1:]))
}
@@ -63,8 +93,3 @@
func main() {
Main(os.Args)
}
-
-func addEnvironFlags(name *string, f *gnuflag.FlagSet) {
- f.StringVar(name, "e", "", "juju environment to operate in")
- f.StringVar(name, "environment", "", "")
-}
=== modified file 'cmd/juju/main_test.go'
--- cmd/juju/main_test.go 2013-02-24 22:23:26 +0000
+++ cmd/juju/main_test.go 2013-04-08 08:09:22 +0000
@@ -1,16 +1,20 @@
package main
import (
+ "bytes"
"flag"
"fmt"
"io/ioutil"
+ "launchpad.net/gnuflag"
. "launchpad.net/gocheck"
+ "launchpad.net/juju-core/cmd"
+ "launchpad.net/juju-core/environs/config"
_ "launchpad.net/juju-core/environs/dummy"
"launchpad.net/juju-core/testing"
+ "launchpad.net/juju-core/version"
"os"
"os/exec"
"path/filepath"
- "sort"
"strings"
stdtesting "testing"
)
@@ -23,7 +27,9 @@
var _ = Suite(&MainSuite{})
-var flagRunMain = flag.Bool("run-main", false, "Run the application's main function for recursive testing")
+var (
+ flagRunMain = flag.Bool("run-main", false, "Run the application's main function for recursive testing")
+)
// Reentrancy point for testing (something as close as possible to) the juju
// tool itself.
@@ -33,9 +39,10 @@
}
}
-func badrun(c *C, exit int, cmd ...string) string {
- args := append([]string{"-test.run", "TestRunMain", "-run-main", "--", "juju"}, cmd...)
- ps := exec.Command(os.Args[0], args...)
+func badrun(c *C, exit int, args ...string) string {
+ localArgs := append([]string{"-test.run", "TestRunMain", "-run-main", "--", "juju"}, args...)
+ ps := exec.Command(os.Args[0], localArgs...)
+ ps.Env = append(os.Environ(), "JUJU_HOME="+config.JujuHome())
output, err := ps.CombinedOutput()
if exit != 0 {
c.Assert(err, ErrorMatches, fmt.Sprintf("exit status %d", exit))
@@ -43,6 +50,24 @@
return string(output)
}
+func helpText(command cmd.Command, name string) string {
+ buff := &bytes.Buffer{}
+ info := command.Info()
+ info.Name = name
+ f := gnuflag.NewFlagSet(info.Name, gnuflag.ContinueOnError)
+ command.SetFlags(f)
+ buff.Write(info.Help(f))
+ return buff.String()
+}
+
+func deployHelpText() string {
+ return helpText(&DeployCommand{}, "juju deploy")
+}
+
+func syncToolsHelpText() string {
+ return helpText(&SyncToolsCommand{}, "juju sync-tools")
+}
+
var runMainTests = []struct {
summary string
args []string
@@ -50,9 +75,45 @@
out string
}{
{
- summary: "missing command",
- code: 2,
- out: "error: no command specified\n",
+ summary: "no params shows help",
+ args: []string{},
+ code: 0,
+ out: strings.TrimLeft(helpBasics, "\n"),
+ }, {
+ summary: "juju help is the same as juju",
+ args: []string{"help"},
+ code: 0,
+ out: strings.TrimLeft(helpBasics, "\n"),
+ }, {
+ summary: "juju --help works too",
+ args: []string{"--help"},
+ code: 0,
+ out: strings.TrimLeft(helpBasics, "\n"),
+ }, {
+ summary: "juju help basics is the same as juju",
+ args: []string{"help", "basics"},
+ code: 0,
+ out: strings.TrimLeft(helpBasics, "\n"),
+ }, {
+ summary: "juju help foo doesn't exist",
+ args: []string{"help", "foo"},
+ code: 1,
+ out: "error: unknown command or topic for foo\n",
+ }, {
+ summary: "juju help deploy shows the default help without global options",
+ args: []string{"help", "deploy"},
+ code: 0,
+ out: deployHelpText(),
+ }, {
+ summary: "juju --help deploy shows the same help as 'help deploy'",
+ args: []string{"--help", "deploy"},
+ code: 0,
+ out: deployHelpText(),
+ }, {
+ summary: "juju deploy --help shows the same help as 'help deploy'",
+ args: []string{"deploy", "--help"},
+ code: 0,
+ out: deployHelpText(),
}, {
summary: "unknown command",
args: []string{"discombobulate"},
@@ -73,10 +134,21 @@
args: []string{"--environment", "blah", "bootstrap"},
code: 2,
out: "error: flag provided but not defined: --environment\n",
+ }, {
+ summary: "juju sync-tools registered properly",
+ args: []string{"sync-tools", "--help"},
+ code: 0,
+ out: syncToolsHelpText(),
+ }, {
+ summary: "check version command registered properly",
+ args: []string{"version"},
+ code: 0,
+ out: version.Current.String() + "\n",
},
}
func (s *MainSuite) TestRunMain(c *C) {
+ defer config.SetJujuHome(config.SetJujuHome(c.MkDir()))
for i, t := range runMainTests {
c.Logf("test %d: %s", i, t.summary)
out := badrun(c, t.code, t.args...)
@@ -93,19 +165,18 @@
broken: %s
`
-// breakJuju forces the dummy environment to return an error
-// when environMethod is called.
+// breakJuju forces the dummy environment to return an error when
+// environMethod is called.
func breakJuju(c *C, environMethod string) (msg string) {
yaml := fmt.Sprintf(brokenConfig, environMethod)
- err := ioutil.WriteFile(homePath(".juju", "environments.yaml"), []byte(yaml), 0666)
+ err := ioutil.WriteFile(config.JujuHomePath("environments.yaml"), []byte(yaml), 0666)
c.Assert(err, IsNil)
return fmt.Sprintf("dummy.%s is broken", environMethod)
}
func (s *MainSuite) TestActualRunJujuArgsBeforeCommand(c *C) {
- defer makeFakeHome(c, "one").restore()
-
+ defer testing.MakeFakeHomeNoEnvironments(c, "one").Restore()
// Check global args work when specified before command
msg := breakJuju(c, "Bootstrap")
logpath := filepath.Join(c.MkDir(), "log")
@@ -113,13 +184,12 @@
c.Assert(out, Equals, "error: "+msg+"\n")
content, err := ioutil.ReadFile(logpath)
c.Assert(err, IsNil)
- fullmsg := fmt.Sprintf(`(.|\n)*JUJU juju bootstrap command failed: %s\n`, msg)
+ fullmsg := fmt.Sprintf(`.*\n.*ERROR JUJU:juju:bootstrap juju bootstrap command failed: %s\n`, msg)
c.Assert(string(content), Matches, fullmsg)
}
func (s *MainSuite) TestActualRunJujuArgsAfterCommand(c *C) {
- defer makeFakeHome(c, "one").restore()
-
+ defer testing.MakeFakeHomeNoEnvironments(c, "one").Restore()
// Check global args work when specified after command
msg := breakJuju(c, "Bootstrap")
logpath := filepath.Join(c.MkDir(), "log")
@@ -127,7 +197,7 @@
c.Assert(out, Equals, "error: "+msg+"\n")
content, err := ioutil.ReadFile(logpath)
c.Assert(err, IsNil)
- fullmsg := fmt.Sprintf(`(.|\n)*JUJU juju bootstrap command failed: %s\n`, msg)
+ fullmsg := fmt.Sprintf(`.*\n.*ERROR JUJU:juju:bootstrap juju bootstrap command failed: %s\n`, msg)
c.Assert(string(content), Matches, fullmsg)
}
@@ -135,6 +205,7 @@
"add-relation",
"add-unit",
"bootstrap",
+ "debug-log",
"deploy",
"destroy-environment",
"destroy-machine",
@@ -145,6 +216,7 @@
"generate-config", // alias for init
"get",
"get-constraints",
+ "help",
"init",
"remove-relation", // alias for destroy-relation
"remove-unit", // alias for destroy-unit
@@ -155,67 +227,84 @@
"ssh",
"stat", // alias for status
"status",
+ "sync-tools",
"terminate-machine", // alias for destroy-machine
"unexpose",
+ "upgrade-charm",
"upgrade-juju",
+ "version",
}
-func (s *MainSuite) TestHelp(c *C) {
+func (s *MainSuite) TestHelpCommands(c *C) {
// Check that we have correctly registered all the commands
// by checking the help output.
-
- out := badrun(c, 0, "-help")
+ defer config.SetJujuHome(config.SetJujuHome(c.MkDir()))
+ out := badrun(c, 0, "help", "commands")
lines := strings.Split(out, "\n")
- c.Assert(lines[0], Matches, `usage: juju .*`)
- for ; len(lines) > 0; lines = lines[1:] {
- if lines[0] == "commands:" {
- break
- }
- }
- c.Assert(lines, Not(HasLen), 0)
-
var names []string
- for lines = lines[1:]; len(lines) > 0; lines = lines[1:] {
- f := strings.Fields(lines[0])
+ for _, line := range lines {
+ f := strings.Fields(line)
if len(f) == 0 {
continue
}
- c.Assert(f, Not(HasLen), 0)
names = append(names, f[0])
}
- sort.Strings(names)
+ // The names should be output in alphabetical order, so don't sort.
c.Assert(names, DeepEquals, commandNames)
}
-type fakeHome string
-
-func makeFakeHome(c *C, certNames ...string) fakeHome {
- oldHome := os.Getenv("HOME")
- os.Setenv("HOME", c.MkDir())
-
- err := os.Mkdir(homePath(".juju"), 0777)
- c.Assert(err, IsNil)
- for _, name := range certNames {
- err := ioutil.WriteFile(homePath(".juju", name+"-cert.pem"), []byte(testing.CACert), 0666)
- c.Assert(err, IsNil)
-
- err = ioutil.WriteFile(homePath(".juju", name+"-private-key.pem"), []byte(testing.CAKey), 0666)
- c.Assert(err, IsNil)
- }
-
- err = os.Mkdir(homePath(".ssh"), 0777)
- c.Assert(err, IsNil)
- err = ioutil.WriteFile(homePath(".ssh", "id_rsa.pub"), []byte("auth key\n"), 0666)
- c.Assert(err, IsNil)
-
- return fakeHome(oldHome)
-}
-
-func homePath(names ...string) string {
- all := append([]string{os.Getenv("HOME")}, names...)
- return filepath.Join(all...)
-}
-
-func (h fakeHome) restore() {
- os.Setenv("HOME", string(h))
+var topicNames = []string{
+ "basics",
+ "commands",
+ "global-options",
+ "topics",
+}
+
+func (s *MainSuite) TestHelpTopics(c *C) {
+ // Check that we have correctly registered all the topics
+ // by checking the help output.
+ defer config.SetJujuHome(config.SetJujuHome(c.MkDir()))
+ out := badrun(c, 0, "help", "topics")
+ lines := strings.Split(out, "\n")
+ var names []string
+ for _, line := range lines {
+ f := strings.Fields(line)
+ if len(f) == 0 {
+ continue
+ }
+ names = append(names, f[0])
+ }
+ // The names should be output in alphabetical order, so don't sort.
+ c.Assert(names, DeepEquals, topicNames)
+}
+
+var globalFlags = []string{
+ "--debug .*",
+ "-h, --help .*",
+ "--log-file .*",
+ "-v, --verbose .*",
+}
+
+func (s *MainSuite) TestHelpGlobalOptions(c *C) {
+ // Check that we have correctly registered all the topics
+ // by checking the help output.
+ defer config.SetJujuHome(config.SetJujuHome(c.MkDir()))
+ out := badrun(c, 0, "help", "global-options")
+ c.Assert(out, Matches, `Global Options
+
+These options may be used with any command, and may appear in front of any
+command\.(.|\n)*`)
+ lines := strings.Split(out, "\n")
+ var flags []string
+ for _, line := range lines {
+ f := strings.Fields(line)
+ if len(f) == 0 || line[0] != '-' {
+ continue
+ }
+ flags = append(flags, line)
+ }
+ c.Assert(len(flags), Equals, len(globalFlags))
+ for i, line := range flags {
+ c.Assert(line, Matches, globalFlags[i])
+ }
}
=== modified file 'cmd/juju/resolved.go'
--- cmd/juju/resolved.go 2013-02-20 22:11:47 +0000
+++ cmd/juju/resolved.go 2013-04-08 08:09:22 +0000
@@ -6,11 +6,13 @@
"launchpad.net/juju-core/cmd"
"launchpad.net/juju-core/juju"
"launchpad.net/juju-core/state"
+ "launchpad.net/juju-core/state/api/params"
+ "launchpad.net/juju-core/state/statecmd"
)
// ResolvedCommand marks a unit in an error state as ready to continue.
type ResolvedCommand struct {
- EnvName string
+ EnvCommandBase
UnitName string
Retry bool
}
@@ -24,7 +26,7 @@
}
func (c *ResolvedCommand) SetFlags(f *gnuflag.FlagSet) {
- addEnvironFlags(&c.EnvName, f)
+ c.EnvCommandBase.SetFlags(f)
f.BoolVar(&c.Retry, "r", false, "re-execute failed hooks")
f.BoolVar(&c.Retry, "retry", false, "")
}
@@ -48,9 +50,9 @@
return err
}
defer conn.Close()
- unit, err := conn.State.Unit(c.UnitName)
- if err != nil {
- return err
+ params := params.Resolved{
+ UnitName: c.UnitName,
+ Retry: c.Retry,
}
- return conn.Resolved(unit, c.Retry)
+ return statecmd.Resolved(conn.State, params)
}
=== modified file 'cmd/juju/resolved_test.go'
--- cmd/juju/resolved_test.go 2013-02-28 12:27:09 +0000
+++ cmd/juju/resolved_test.go 2013-04-08 08:09:22 +0000
@@ -2,7 +2,7 @@
import (
. "launchpad.net/gocheck"
- "launchpad.net/juju-core/state"
+ "launchpad.net/juju-core/state/api/params"
"launchpad.net/juju-core/testing"
)
@@ -21,7 +21,7 @@
args []string
err string
unit string
- mode state.ResolvedMode
+ mode params.ResolvedMode
}{
{
err: `no unit specified`,
@@ -35,30 +35,30 @@
args: []string{"dummy/0"},
err: `unit "dummy/0" is not in an error state`,
unit: "dummy/0",
- mode: state.ResolvedNone,
+ mode: params.ResolvedNone,
}, {
args: []string{"dummy/1", "--retry"},
err: `unit "dummy/1" is not in an error state`,
unit: "dummy/1",
- mode: state.ResolvedNone,
+ mode: params.ResolvedNone,
}, {
args: []string{"dummy/2"},
unit: "dummy/2",
- mode: state.ResolvedNoHooks,
+ mode: params.ResolvedNoHooks,
}, {
args: []string{"dummy/2", "--retry"},
err: `cannot set resolved mode for unit "dummy/2": already resolved`,
unit: "dummy/2",
- mode: state.ResolvedNoHooks,
+ mode: params.ResolvedNoHooks,
}, {
args: []string{"dummy/3", "--retry"},
unit: "dummy/3",
- mode: state.ResolvedRetryHooks,
+ mode: params.ResolvedRetryHooks,
}, {
args: []string{"dummy/3"},
err: `cannot set resolved mode for unit "dummy/3": already resolved`,
unit: "dummy/3",
- mode: state.ResolvedRetryHooks,
+ mode: params.ResolvedRetryHooks,
}, {
args: []string{"dummy/4", "roflcopter"},
err: `unrecognized args: \["roflcopter"\]`,
@@ -73,7 +73,7 @@
for _, name := range []string{"dummy/2", "dummy/3", "dummy/4"} {
u, err := s.State.Unit(name)
c.Assert(err, IsNil)
- err = u.SetStatus(state.UnitError, "lol borken")
+ err = u.SetStatus(params.UnitError, "lol borken")
c.Assert(err, IsNil)
}
=== modified file 'cmd/juju/scp.go'
--- cmd/juju/scp.go 2013-02-20 22:11:47 +0000
+++ cmd/juju/scp.go 2013-04-08 08:09:22 +0000
@@ -5,7 +5,6 @@
"os/exec"
"strings"
- "launchpad.net/gnuflag"
"launchpad.net/juju-core/cmd"
"launchpad.net/juju-core/juju"
)
@@ -23,10 +22,6 @@
}
}
-func (c *SCPCommand) SetFlags(f *gnuflag.FlagSet) {
- addEnvironFlags(&c.EnvName, f)
-}
-
func (c *SCPCommand) Init(args []string) error {
switch len(args) {
case 0, 1:
=== modified file 'cmd/juju/set.go'
--- cmd/juju/set.go 2013-02-26 18:36:25 +0000
+++ cmd/juju/set.go 2013-04-08 08:09:22 +0000
@@ -8,12 +8,12 @@
"launchpad.net/gnuflag"
"launchpad.net/juju-core/cmd"
"launchpad.net/juju-core/juju"
- "launchpad.net/juju-core/state/statecmd"
+ "launchpad.net/juju-core/state/api/params"
)
// SetCommand updates the configuration of a service
type SetCommand struct {
- EnvName string
+ EnvCommandBase
ServiceName string
// either Options or Config will contain the configuration data
Options []string
@@ -30,7 +30,7 @@
}
func (c *SetCommand) SetFlags(f *gnuflag.FlagSet) {
- addEnvironFlags(&c.EnvName, f)
+ c.EnvCommandBase.SetFlags(f)
f.Var(&c.Config, "config", "path to yaml-formatted service config")
}
@@ -67,18 +67,17 @@
return err
}
defer conn.Close()
+
if len(contents) == 0 {
- err = statecmd.ServiceSet(conn.State, statecmd.ServiceSetParams{
+ return juju.ServiceSet(conn.State, params.ServiceSet{
ServiceName: c.ServiceName,
Options: options,
})
- } else {
- err = statecmd.ServiceSetYAML(conn.State, statecmd.ServiceSetYAMLParams{
- ServiceName: c.ServiceName,
- Config: string(contents),
- })
}
- return err
+ return juju.ServiceSetYAML(conn.State, params.ServiceSetYAML{
+ ServiceName: c.ServiceName,
+ Config: string(contents),
+ })
}
// parse parses the option k=v strings into a map of options to be
=== modified file 'cmd/juju/ssh.go'
--- cmd/juju/ssh.go 2013-02-20 22:11:47 +0000
+++ cmd/juju/ssh.go 2013-04-08 08:09:22 +0000
@@ -3,7 +3,6 @@
import (
"errors"
"fmt"
- "launchpad.net/gnuflag"
"launchpad.net/juju-core/cmd"
"launchpad.net/juju-core/juju"
"launchpad.net/juju-core/log"
@@ -18,9 +17,9 @@
// SSHCommon provides common methods for SSHCommand and SCPCommand.
type SSHCommon struct {
- EnvName string
- Target string
- Args []string
+ EnvCommandBase
+ Target string
+ Args []string
*juju.Conn
}
@@ -39,10 +38,6 @@
}
}
-func (c *SSHCommand) SetFlags(f *gnuflag.FlagSet) {
- addEnvironFlags(&c.EnvName, f)
-}
-
func (c *SSHCommand) Init(args []string) error {
if len(args) == 0 {
return errors.New("no service name specified")
@@ -77,13 +72,13 @@
func (c *SSHCommon) hostFromTarget(target string) (string, error) {
// is the target the id of a machine ?
if state.IsMachineId(target) {
- log.Printf("cmd/juju: looking up address for machine %s...", target)
+ log.Infof("cmd/juju: looking up address for machine %s...", target)
// TODO(dfc) maybe we should have machine.PublicAddress() ?
return c.machinePublicAddress(target)
}
// maybe the target is a unit ?
if state.IsUnitName(target) {
- log.Printf("cmd/juju: Looking up address for unit %q...", c.Target)
+ log.Infof("cmd/juju: looking up address for unit %q...", c.Target)
unit, err := c.State.Unit(target)
if err != nil {
return "", err
=== modified file 'cmd/juju/ssh_test.go'
--- cmd/juju/ssh_test.go 2013-02-28 12:27:09 +0000
+++ cmd/juju/ssh_test.go 2013-04-08 08:09:22 +0000
@@ -9,7 +9,6 @@
"launchpad.net/juju-core/juju/testing"
"launchpad.net/juju-core/state"
coretesting "launchpad.net/juju-core/testing"
- "launchpad.net/juju-core/version"
"net/url"
"os"
"path/filepath"
@@ -118,12 +117,11 @@
func (s *SSHCommonSuite) makeMachines(n int, c *C) []*state.Machine {
var machines = make([]*state.Machine, n)
for i := 0; i < n; i++ {
- m, err := s.State.AddMachine(version.Current.Series, state.JobHostUnits)
+ m, err := s.State.AddMachine("series", state.JobHostUnits)
c.Assert(err, IsNil)
// must set an instance id as the ssh command uses that as a signal the machine
// has been provisioned
- inst, err := s.Conn.Environ.StartInstance(m.Id(), testing.InvalidStateInfo(m.Id()), testing.InvalidAPIInfo(m.Id()), nil)
- c.Assert(err, IsNil)
+ inst := testing.StartInstance(c, s.Conn.Environ, m.Id())
c.Assert(m.SetInstanceId(inst.Id()), IsNil)
machines[i] = m
}
=== modified file 'cmd/juju/status.go'
--- cmd/juju/status.go 2013-02-25 16:20:39 +0000
+++ cmd/juju/status.go 2013-04-08 08:09:22 +0000
@@ -7,11 +7,12 @@
"launchpad.net/juju-core/environs"
"launchpad.net/juju-core/juju"
"launchpad.net/juju-core/state"
+ "launchpad.net/juju-core/state/api/params"
)
type StatusCommand struct {
- EnvName string
- out cmd.Output
+ EnvCommandBase
+ out cmd.Output
}
var statusDoc = "This command will report on the runtime state of various system entities."
@@ -26,17 +27,13 @@
}
func (c *StatusCommand) SetFlags(f *gnuflag.FlagSet) {
- addEnvironFlags(&c.EnvName, f)
+ c.EnvCommandBase.SetFlags(f)
c.out.AddFlags(f, "yaml", map[string]cmd.Formatter{
"yaml": cmd.FormatYaml,
"json": cmd.FormatJson,
})
}
-func (c *StatusCommand) Init(args []string) error {
- return cmd.CheckEmpty(args)
-}
-
func (c *StatusCommand) Run(ctx *cmd.Context) error {
conn, err := juju.NewConnFromName(c.EnvName)
if err != nil {
@@ -201,7 +198,26 @@
}
processVersion(r, unit)
- processStatus(r, unit)
+
+ agentAlive, err := unit.AgentAlive()
+ if err != nil {
+ return nil, err
+ }
+ unitDead := unit.Life() == state.Dead
+ status, info, err := unit.Status()
+ if err != nil {
+ return nil, err
+ }
+ if status != params.UnitPending {
+ if !agentAlive && !unitDead {
+ // Agent should be running but it's not.
+ status = params.UnitDown
+ }
+ }
+ r["agent-state"] = status
+ if len(info) > 0 {
+ r["agent-state-info"] = info
+ }
return r, nil
}
@@ -215,19 +231,6 @@
}
}
-type status interface {
- Status() (state.UnitStatus, string, error)
-}
-
-func processStatus(r map[string]interface{}, s status) {
- if status, info, err := s.Status(); err == nil {
- r["status"] = status
- if len(info) > 0 {
- r["status-info"] = info
- }
- }
-}
-
type agentAliver interface {
AgentAlive() (bool, error)
}
=== modified file 'cmd/juju/status_test.go'
--- cmd/juju/status_test.go 2013-02-28 12:27:09 +0000
+++ cmd/juju/status_test.go 2013-04-08 08:09:22 +0000
@@ -11,265 +11,434 @@
"launchpad.net/juju-core/juju"
"launchpad.net/juju-core/juju/testing"
"launchpad.net/juju-core/state"
+ "launchpad.net/juju-core/state/api/params"
+ "launchpad.net/juju-core/state/presence"
coretesting "launchpad.net/juju-core/testing"
"launchpad.net/juju-core/version"
"net/url"
- "strconv"
+ "time"
)
+func runStatus(c *C, args ...string) (code int, stdout, stderr []byte) {
+ ctx := coretesting.Context(c)
+ code = cmd.Main(&StatusCommand{}, ctx, args)
+ stdout = ctx.Stdout.(*bytes.Buffer).Bytes()
+ stderr = ctx.Stderr.(*bytes.Buffer).Bytes()
+ return
+}
+
type StatusSuite struct {
testing.JujuConnSuite
}
var _ = Suite(&StatusSuite{})
-var statusTests = []struct {
- title string
- prepare func(*state.State, *juju.Conn, *C)
- output map[string]interface{}
-}{
- {
+type M map[string]interface{}
+
+type testCase struct {
+ summary string
+ steps []stepper
+}
+
+func test(summary string, steps ...stepper) testCase {
+ return testCase{summary, steps}
+}
+
+type stepper interface {
+ step(c *C, ctx *context)
+}
+
+type context struct {
+ st *state.State
+ conn *juju.Conn
+ charms map[string]*state.Charm
+ unitPingers map[string]*presence.Pinger
+}
+
+func (s *StatusSuite) newContext() *context {
+ return &context{
+ st: s.State,
+ conn: s.Conn,
+ charms: make(map[string]*state.Charm),
+ unitPingers: make(map[string]*presence.Pinger),
+ }
+}
+
+func (s *StatusSuite) resetContext(c *C, ctx *context) {
+ for _, up := range ctx.unitPingers {
+ err := up.Kill()
+ c.Check(err, IsNil)
+ }
+ s.JujuConnSuite.Reset(c)
+}
+
+func (ctx *context) run(c *C, steps []stepper) {
+ for i, s := range steps {
+ c.Logf("step %d", i)
+ c.Logf("%#v", s)
+ s.step(c, ctx)
+ }
+}
+
+// shortcuts for expected output.
+var (
+ machine0 = M{
+ "dns-name": "dummyenv-0.dns",
+ "instance-id": "dummyenv-0",
+ }
+ machine1 = M{
+ "dns-name": "dummyenv-1.dns",
+ "instance-id": "dummyenv-1",
+ }
+ machine2 = M{
+ "dns-name": "dummyenv-2.dns",
+ "instance-id": "dummyenv-2",
+ }
+ unexposedService = M{
+ "charm": "local:series/dummy-1",
+ "exposed": false,
+ }
+ exposedService = M{
+ "charm": "local:series/dummy-1",
+ "exposed": true,
+ }
+)
+
+type outputFormat struct {
+ name string
+ marshal func(v interface{}) ([]byte, error)
+ unmarshal func(data []byte, v interface{}) error
+}
+
+// statusFormats list all output formats supported by status command.
+var statusFormats = []outputFormat{
+ {"yaml", goyaml.Marshal, goyaml.Unmarshal},
+ {"json", json.Marshal, json.Unmarshal},
+}
+
+var statusTests = []testCase{
+ test(
+ "bootstrap and starting a single instance",
+
// unlikely, as you can't run juju status in real life without
// machine/0 bootstrapped.
- "empty state",
- func(*state.State, *juju.Conn, *C) {},
- map[string]interface{}{
- "machines": make(map[string]interface{}),
- "services": make(map[string]interface{}),
- },
- },
- {
- "simulate juju bootstrap by adding machine/0 to the state",
- func(st *state.State, _ *juju.Conn, c *C) {
- m, err := st.AddMachine(version.Current.Series, state.JobManageEnviron)
- c.Assert(err, IsNil)
- c.Assert(m.Id(), Equals, "0")
- },
- map[string]interface{}{
- "machines": map[string]interface{}{
- "0": map[string]interface{}{
- "instance-id": "pending",
- },
- },
- "services": make(map[string]interface{}),
- },
- },
- {
- "simulate the PA starting an instance in response to the state change",
- func(st *state.State, conn *juju.Conn, c *C) {
- m, err := st.Machine("0")
- c.Assert(err, IsNil)
- inst, err := conn.Environ.StartInstance(m.Id(), testing.InvalidStateInfo(m.Id()), testing.InvalidAPIInfo(m.Id()), nil)
- c.Assert(err, IsNil)
- err = m.SetInstanceId(inst.Id())
- c.Assert(err, IsNil)
- },
- map[string]interface{}{
- "machines": map[string]interface{}{
- "0": map[string]interface{}{
- "dns-name": "dummyenv-0.dns",
- "instance-id": "dummyenv-0",
- },
- },
- "services": make(map[string]interface{}),
- },
- },
- {
- "simulate the MA setting the version",
- func(st *state.State, conn *juju.Conn, c *C) {
- m, err := st.Machine("0")
- c.Assert(err, IsNil)
- t := &state.Tools{
- Binary: version.Binary{
- Number: version.MustParse("1.2.3"),
- Series: "gutsy",
- Arch: "ppc",
- },
- URL: "http://canonical.com/",
- }
- err = m.SetAgentTools(t)
- c.Assert(err, IsNil)
- },
- map[string]interface{}{
- "machines": map[string]interface{}{
- "0": map[string]interface{}{
- "dns-name": "dummyenv-0.dns",
- "instance-id": "dummyenv-0",
- "agent-version": "1.2.3",
- },
- },
- "services": make(map[string]interface{}),
- },
- },
- {
- "add two services and expose one",
- func(st *state.State, conn *juju.Conn, c *C) {
- ch := coretesting.Charms.Dir("dummy")
- curl := charm.MustParseURL(
- fmt.Sprintf("local:series/%s-%d", ch.Meta().Name, ch.Revision()),
- )
- bundleURL, err := url.Parse("http://bundles.example.com/dummy-1")
- c.Assert(err, IsNil)
- dummy, err := st.AddCharm(ch, curl, bundleURL, "dummy-1-sha256")
- c.Assert(err, IsNil)
- _, err = st.AddService("dummy-service", dummy)
- c.Assert(err, IsNil)
- s, err := st.AddService("exposed-service", dummy)
- c.Assert(err, IsNil)
- err = s.SetExposed()
- c.Assert(err, IsNil)
- },
- map[string]interface{}{
- "machines": map[string]interface{}{
- "0": map[string]interface{}{
- "dns-name": "dummyenv-0.dns",
- "instance-id": "dummyenv-0",
- "agent-version": "1.2.3",
- },
- },
- "services": map[string]interface{}{
- "dummy-service": map[string]interface{}{
- "charm": "local:series/dummy-1",
- "exposed": false,
- },
- "exposed-service": map[string]interface{}{
- "charm": "local:series/dummy-1",
- "exposed": true,
- },
- },
- },
- },
- {
- "add three more machines for units",
- func(st *state.State, conn *juju.Conn, c *C) {
- for i := 1; i < 3; i++ {
- m, err := st.AddMachine(version.Current.Series, state.JobHostUnits)
- c.Assert(err, IsNil)
- c.Assert(m.Id(), Equals, strconv.Itoa(i))
- inst, err := conn.Environ.StartInstance(m.Id(), testing.InvalidStateInfo(m.Id()), testing.InvalidAPIInfo(m.Id()), nil)
- c.Assert(err, IsNil)
- err = m.SetInstanceId(inst.Id())
- c.Assert(err, IsNil)
- }
- },
- map[string]interface{}{
- "machines": map[string]interface{}{
- "0": map[string]interface{}{
- "dns-name": "dummyenv-0.dns",
- "instance-id": "dummyenv-0",
- "agent-version": "1.2.3",
- },
- "1": map[string]interface{}{
- "dns-name": "dummyenv-1.dns",
- "instance-id": "dummyenv-1",
- },
- "2": map[string]interface{}{
- "dns-name": "dummyenv-2.dns",
- "instance-id": "dummyenv-2",
- },
- },
- "services": map[string]interface{}{
- "dummy-service": map[string]interface{}{
- "charm": "local:series/dummy-1",
- "exposed": false,
- },
- "exposed-service": map[string]interface{}{
- "charm": "local:series/dummy-1",
- "exposed": true,
- },
- },
- },
- },
- {
- "add units for services",
- func(st *state.State, conn *juju.Conn, c *C) {
- for i, n := range []string{"dummy-service", "exposed-service"} {
- s, err := st.Service(n)
- c.Assert(err, IsNil)
- u, err := s.AddUnit()
- c.Assert(err, IsNil)
- m, err := st.Machine(strconv.Itoa(i + 1))
- c.Assert(err, IsNil)
- err = u.AssignToMachine(m)
- c.Assert(err, IsNil)
-
- if n == "exposed-service" {
- err := u.SetStatus("error", "You Require More Vespene Gas")
- c.Assert(err, IsNil)
- }
- }
- },
- map[string]interface{}{
- "machines": map[string]interface{}{
- "0": map[string]interface{}{
- "dns-name": "dummyenv-0.dns",
- "instance-id": "dummyenv-0",
- "agent-version": "1.2.3",
- },
- "1": map[string]interface{}{
- "dns-name": "dummyenv-1.dns",
- "instance-id": "dummyenv-1",
- },
- "2": map[string]interface{}{
- "dns-name": "dummyenv-2.dns",
- "instance-id": "dummyenv-2",
- },
- },
- "services": map[string]interface{}{
- "exposed-service": map[string]interface{}{
- "exposed": true,
- "units": map[string]interface{}{
- "exposed-service/0": map[string]interface{}{
- "machine": "2",
- "status": "error",
- "status-info": "You Require More Vespene Gas",
- },
- },
- "charm": "local:series/dummy-1",
- },
- "dummy-service": map[string]interface{}{
- "charm": "local:series/dummy-1",
- "exposed": false,
- "units": map[string]interface{}{
- "dummy-service/0": map[string]interface{}{
- "machine": "1",
- "status": "pending",
- },
- },
- },
- },
- },
- },
-
- // TODO(dfc) test failing components by destructively mutating the state under the hood
-}
-
-func (s *StatusSuite) testStatus(format string, marshal func(v interface{}) ([]byte, error), unmarshal func(data []byte, v interface{}) error, c *C) {
- for _, t := range statusTests {
- c.Logf("testing %s: %s", format, t.title)
- t.prepare(s.State, s.Conn, c)
- ctx := coretesting.Context(c)
- code := cmd.Main(&StatusCommand{}, ctx, []string{"--format", format})
- c.Check(code, Equals, 0)
- c.Assert(ctx.Stderr.(*bytes.Buffer).String(), Equals, "")
-
- buf, err := marshal(t.output)
- c.Assert(err, IsNil)
- expected := make(map[string]interface{})
- err = unmarshal(buf, &expected)
- c.Assert(err, IsNil)
-
- actual := make(map[string]interface{})
- err = unmarshal(ctx.Stdout.(*bytes.Buffer).Bytes(), &actual)
+ expect{
+ "empty state",
+ M{
+ "machines": M{},
+ "services": M{},
+ },
+ },
+
+ addMachine{"0", state.JobManageEnviron},
+ expect{
+ "simulate juju bootstrap by adding machine/0 to the state",
+ M{
+ "machines": M{
+ "0": M{
+ "instance-id": "pending",
+ },
+ },
+ "services": M{},
+ },
+ },
+
+ startMachine{"0"},
+ expect{
+ "simulate the PA starting an instance in response to the state change",
+ M{
+ "machines": M{
+ "0": machine0,
+ },
+ "services": M{},
+ },
+ },
+
+ setTools{"0", &state.Tools{
+ Binary: version.Binary{
+ Number: version.MustParse("1.2.3"),
+ Series: "gutsy",
+ Arch: "ppc",
+ },
+ URL: "http://canonical.com/",
+ }},
+ expect{
+ "simulate the MA setting the version",
+ M{
+ "machines": M{
+ "0": M{
+ "dns-name": "dummyenv-0.dns",
+ "instance-id": "dummyenv-0",
+ "agent-version": "1.2.3",
+ },
+ },
+ "services": M{},
+ },
+ },
+ ), test(
+ "add two services and expose one, then add 2 more machines and some units",
+ addMachine{"0", state.JobManageEnviron},
+ startMachine{"0"},
+ addCharm{"dummy"},
+ addService{"dummy-service", "dummy"},
+ addService{"exposed-service", "dummy"},
+ expect{
+ "no services exposed yet",
+ M{
+ "machines": M{
+ "0": machine0,
+ },
+ "services": M{
+ "dummy-service": unexposedService,
+ "exposed-service": unexposedService,
+ },
+ },
+ },
+
+ setServiceExposed{"exposed-service", true},
+ expect{
+ "one exposed service",
+ M{
+ "machines": M{
+ "0": machine0,
+ },
+ "services": M{
+ "dummy-service": unexposedService,
+ "exposed-service": exposedService,
+ },
+ },
+ },
+
+ addMachine{"1", state.JobHostUnits},
+ startMachine{"1"},
+ addMachine{"2", state.JobHostUnits},
+ startMachine{"2"},
+ expect{
+ "two more machines added",
+ M{
+ "machines": M{
+ "0": machine0,
+ "1": machine1,
+ "2": machine2,
+ },
+ "services": M{
+ "dummy-service": unexposedService,
+ "exposed-service": exposedService,
+ },
+ },
+ },
+
+ addUnit{"dummy-service", "1"},
+ addAliveUnit{"exposed-service", "2"},
+ setUnitStatus{"exposed-service/0", params.UnitError, "You Require More Vespene Gas"},
+ // This will be ignored, because the unit is down.
+ setUnitStatus{"dummy-service/0", params.UnitStarted, ""},
+ expect{
+ "add two units, one alive (in error state), one down",
+ M{
+ "machines": M{
+ "0": machine0,
+ "1": machine1,
+ "2": machine2,
+ },
+ "services": M{
+ "exposed-service": M{
+ "charm": "local:series/dummy-1",
+ "exposed": true,
+ "units": M{
+ "exposed-service/0": M{
+ "machine": "2",
+ "agent-state": "error",
+ "agent-state-info": "You Require More Vespene Gas",
+ },
+ },
+ },
+ "dummy-service": M{
+ "charm": "local:series/dummy-1",
+ "exposed": false,
+ "units": M{
+ "dummy-service/0": M{
+ "machine": "1",
+ "agent-state": "down",
+ },
+ },
+ },
+ },
+ },
+ },
+ ),
+}
+
+// TODO(dfc) test failing components by destructively mutating the state under the hood
+
+type addMachine struct {
+ machineId string
+ job state.MachineJob
+}
+
+func (am addMachine) step(c *C, ctx *context) {
+ m, err := ctx.st.AddMachine("series", am.job)
+ c.Assert(err, IsNil)
+ c.Assert(m.Id(), Equals, am.machineId)
+}
+
+type startMachine struct {
+ machineId string
+}
+
+func (sm startMachine) step(c *C, ctx *context) {
+ m, err := ctx.st.Machine(sm.machineId)
+ c.Assert(err, IsNil)
+ inst := testing.StartInstance(c, ctx.conn.Environ, m.Id())
+ err = m.SetInstanceId(inst.Id())
+ c.Assert(err, IsNil)
+}
+
+type setTools struct {
+ machineId string
+ tools *state.Tools
+}
+
+func (st setTools) step(c *C, ctx *context) {
+ m, err := ctx.st.Machine(st.machineId)
+ c.Assert(err, IsNil)
+ err = m.SetAgentTools(st.tools)
+ c.Assert(err, IsNil)
+}
+
+type addCharm struct {
+ name string
+}
+
+func (ac addCharm) step(c *C, ctx *context) {
+ ch := coretesting.Charms.Dir(ac.name)
+ name, rev := ch.Meta().Name, ch.Revision()
+ curl := charm.MustParseURL(fmt.Sprintf("local:series/%s-%d", name, rev))
+ bundleURL, err := url.Parse(fmt.Sprintf("http://bundles.example.com/%s-%d", name, rev))
+ c.Assert(err, IsNil)
+ dummy, err := ctx.st.AddCharm(ch, curl, bundleURL, fmt.Sprintf("%s-%d-sha256", name, rev))
+ c.Assert(err, IsNil)
+ ctx.charms[ac.name] = dummy
+}
+
+type addService struct {
+ name string
+ charm string
+}
+
+func (as addService) step(c *C, ctx *context) {
+ ch, ok := ctx.charms[as.charm]
+ c.Assert(ok, Equals, true)
+ _, err := ctx.st.AddService(as.name, ch)
+ c.Assert(err, IsNil)
+}
+
+type setServiceExposed struct {
+ name string
+ exposed bool
+}
+
+func (sse setServiceExposed) step(c *C, ctx *context) {
+ s, err := ctx.st.Service(sse.name)
+ c.Assert(err, IsNil)
+ if sse.exposed {
+ err = s.SetExposed()
+ c.Assert(err, IsNil)
+ }
+}
+
+type addUnit struct {
+ serviceName string
+ machineId string
+}
+
+func (au addUnit) step(c *C, ctx *context) {
+ s, err := ctx.st.Service(au.serviceName)
+ c.Assert(err, IsNil)
+ u, err := s.AddUnit()
+ c.Assert(err, IsNil)
+ m, err := ctx.st.Machine(au.machineId)
+ c.Assert(err, IsNil)
+ err = u.AssignToMachine(m)
+ c.Assert(err, IsNil)
+}
+
+type addAliveUnit struct {
+ serviceName string
+ machineId string
+}
+
+func (aau addAliveUnit) step(c *C, ctx *context) {
+ s, err := ctx.st.Service(aau.serviceName)
+ c.Assert(err, IsNil)
+ u, err := s.AddUnit()
+ c.Assert(err, IsNil)
+ pinger, err := u.SetAgentAlive()
+ c.Assert(err, IsNil)
+ ctx.st.StartSync()
+ err = u.WaitAgentAlive(200 * time.Millisecond)
+ c.Assert(err, IsNil)
+ agentAlive, err := u.AgentAlive()
+ c.Assert(err, IsNil)
+ c.Assert(agentAlive, Equals, true)
+ m, err := ctx.st.Machine(aau.machineId)
+ c.Assert(err, IsNil)
+ err = u.AssignToMachine(m)
+ c.Assert(err, IsNil)
+ ctx.unitPingers[u.Name()] = pinger
+}
+
+type setUnitStatus struct {
+ unitName string
+ status params.UnitStatus
+ statusInfo string
+}
+
+func (sus setUnitStatus) step(c *C, ctx *context) {
+ u, err := ctx.st.Unit(sus.unitName)
+ err = u.SetStatus(sus.status, sus.statusInfo)
+ c.Assert(err, IsNil)
+}
+
+type expect struct {
+ what string
+ output M
+}
+
+func (e expect) step(c *C, ctx *context) {
+ c.Log("expect: %s", e.what)
+
+ // Now execute the command for each format.
+ for _, format := range statusFormats {
+ c.Logf("format %q", format.name)
+ // Run command with the required format.
+ code, stdout, stderr := runStatus(c, "--format", format.name)
+ c.Assert(code, Equals, 0)
+ c.Assert(stderr, HasLen, 0)
+
+ // Prepare the output in the same format.
+ buf, err := format.marshal(e.output)
+ c.Assert(err, IsNil)
+ expected := make(M)
+ err = format.unmarshal(buf, &expected)
+ c.Assert(err, IsNil)
+
+ // Check the output is as expected.
+ actual := make(M)
+ err = format.unmarshal(stdout, &actual)
c.Assert(err, IsNil)
c.Assert(actual, DeepEquals, expected)
}
}
-func (s *StatusSuite) TestYamlStatus(c *C) {
- s.testStatus("yaml", goyaml.Marshal, goyaml.Unmarshal, c)
-}
-
-func (s *StatusSuite) TestJsonStatus(c *C) {
- s.testStatus("json", json.Marshal, json.Unmarshal, c)
+func (s *StatusSuite) TestStatusAllFormats(c *C) {
+ for i, t := range statusTests {
+ c.Log("test %d: %s", i, t.summary)
+ func() {
+ // Prepare context and run all steps to setup.
+ ctx := s.newContext()
+ defer s.resetContext(c, ctx)
+ ctx.run(c, t.steps)
+ }()
+ }
}
=== added file 'cmd/juju/sync_tools.go'
--- cmd/juju/sync_tools.go 1970-01-01 00:00:00 +0000
+++ cmd/juju/sync_tools.go 2013-04-08 08:09:22 +0000
@@ -0,0 +1,192 @@
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "launchpad.net/gnuflag"
+ "launchpad.net/juju-core/cmd"
+ "launchpad.net/juju-core/environs"
+ "launchpad.net/juju-core/log"
+ "launchpad.net/juju-core/state"
+ "launchpad.net/juju-core/version"
+)
+
+// SyncToolsCommand copies all the tools from the us-east-1 bucket to the local
+// bucket.
+type SyncToolsCommand struct {
+ EnvCommandBase
+ allVersions bool
+ dryRun bool
+ publicBucket bool
+}
+
+var _ cmd.Command = (*SyncToolsCommand)(nil)
+
+func (c *SyncToolsCommand) Info() *cmd.Info {
+ return &cmd.Info{
+ Name: "sync-tools",
+ Purpose: "copy tools from the official bucket into a local environment",
+ Doc: `
+This copies the Juju tools tarball from the official bucket into
+your environment. This is generally done when you want Juju to be able
+to run without having to access Amazon. Sometimes this is because the
+environment does not have public access, and sometimes you just want
+to avoid having to access data outside of the local cloud.
+`,
+ }
+}
+
+func (c *SyncToolsCommand) SetFlags(f *gnuflag.FlagSet) {
+ c.EnvCommandBase.SetFlags(f)
+ f.BoolVar(&c.allVersions, "all", false, "copy all versions, not just the latest")
+ f.BoolVar(&c.dryRun, "dry-run", false, "don't copy, just print what would be copied")
+ f.BoolVar(&c.publicBucket, "public", false, "write to the public-bucket of the account, instead of the bucket private to the environment.")
+
+ // BUG(lp:1163164) jam 2013-04-2 we would like to add a "source"
+ // location, rather than only copying from us-east-1
+}
+
+func (c *SyncToolsCommand) Init(args []string) error {
+ return cmd.CheckEmpty(args)
+}
+
+var officialBucketAttrs = map[string]interface{}{
+ "name": "juju-public",
+ "type": "ec2",
+ "control-bucket": "juju-dist",
+ "access-key": "",
+ "secret-key": "",
+ "authorized-keys": "not-really", // We shouldn't need ssh access
+}
+
+// Find the set of tools at the 'latest' version
+func findNewest(fullTools []*state.Tools) []*state.Tools {
+ // This assumes the zero version of Number is always less than a real
+ // number, but we don't have negative versions, so this should be fine
+ var curBest version.Number
+ var res []*state.Tools
+ for _, tool := range fullTools {
+ if curBest.Less(tool.Number) {
+ // This tool is newer than our current best,
+ // so reset the list
+ res = []*state.Tools{tool}
+ curBest = tool.Number
+ } else if curBest == tool.Number {
+ res = append(res, tool)
+ }
+ }
+ return res
+}
+
+// Find tools that aren't present in target
+func findMissing(sourceTools, targetTools []*state.Tools) []*state.Tools {
+ target := make(map[version.Binary]bool, len(targetTools))
+ for _, tool := range targetTools {
+ target[tool.Binary] = true
+ }
+ var res []*state.Tools
+ for _, tool := range sourceTools {
+ if !target[tool.Binary] {
+ res = append(res, tool)
+ }
+ }
+ return res
+}
+
+func copyOne(
+ tool *state.Tools, source environs.StorageReader,
+ target environs.Storage, ctx *cmd.Context,
+) error {
+ toolsPath := environs.ToolsStoragePath(tool.Binary)
+ fmt.Fprintf(ctx.Stderr, "copying %v", toolsPath)
+ srcFile, err := source.Get(toolsPath)
+ if err != nil {
+ return err
+ }
+ defer srcFile.Close()
+ // We have to buffer the content, because Put requires the content
+ // length, but Get only returns us a ReadCloser
+ buf := &bytes.Buffer{}
+ nBytes, err := io.Copy(buf, srcFile)
+ if err != nil {
+ return err
+ }
+ log.Infof("cmd/juju: downloaded %v (%dkB), uploading", toolsPath, (nBytes+512)/1024)
+ fmt.Fprintf(ctx.Stderr, ", download %dkB, uploading\n", (nBytes+512)/1024)
+
+ if err := target.Put(toolsPath, buf, nBytes); err != nil {
+ return err
+ }
+ return nil
+}
+
+func copyTools(
+ tools []*state.Tools, source environs.StorageReader,
+ target environs.Storage, dryRun bool, ctx *cmd.Context,
+) error {
+ for _, tool := range tools {
+ log.Infof("cmd/juju: copying %s from %s", tool.Binary, tool.URL)
+ if dryRun {
+ continue
+ }
+ if err := copyOne(tool, source, target, ctx); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (c *SyncToolsCommand) Run(ctx *cmd.Context) error {
+ officialEnviron, err := environs.NewFromAttrs(officialBucketAttrs)
+ if err != nil {
+ log.Errorf("cmd/juju: failed to initialize the official bucket environment")
+ return err
+ }
+ fmt.Fprintf(ctx.Stderr, "listing the source bucket\n")
+ sourceToolsList, err := environs.ListTools(officialEnviron, version.Current.Major)
+ if err != nil {
+ return err
+ }
+ targetEnv, err := environs.NewFromName(c.EnvName)
+ if err != nil {
+ log.Errorf("cmd/juju: unable to read %q from environment", c.EnvName)
+ return err
+ }
+ toolsToCopy := sourceToolsList.Public
+ if !c.allVersions {
+ toolsToCopy = findNewest(toolsToCopy)
+ }
+ fmt.Fprintf(ctx.Stderr, "found %d tools in source (%d recent ones)\n",
+ len(sourceToolsList.Public), len(toolsToCopy))
+ for _, tool := range toolsToCopy {
+ log.Debugf("cmd/juju: found source tool: %s", tool)
+ }
+ fmt.Fprintf(ctx.Stderr, "listing target bucket\n")
+ targetToolsList, err := environs.ListTools(targetEnv, version.Current.Major)
+ if err != nil {
+ return err
+ }
+ for _, tool := range targetToolsList.Private {
+ log.Debugf("cmd/juju: found target tool: %s", tool)
+ }
+ targetTools := targetToolsList.Private
+ targetStorage := targetEnv.Storage()
+ if c.publicBucket {
+ targetTools = targetToolsList.Public
+ var ok bool
+ if targetStorage, ok = targetEnv.PublicStorage().(environs.Storage); !ok {
+ return fmt.Errorf("Cannot write to PublicStorage")
+ }
+
+ }
+ missing := findMissing(toolsToCopy, targetTools)
+ fmt.Fprintf(ctx.Stdout, "found %d tools in target; %d tools to be copied\n",
+ len(targetTools), len(missing))
+ err = copyTools(missing, officialEnviron.PublicStorage(), targetStorage, c.dryRun, ctx)
+ if err != nil {
+ return err
+ }
+ fmt.Fprintf(ctx.Stderr, "copied %d tools\n", len(missing))
+ return nil
+}
=== added file 'cmd/juju/sync_tools_test.go'
--- cmd/juju/sync_tools_test.go 1970-01-01 00:00:00 +0000
+++ cmd/juju/sync_tools_test.go 2013-04-08 08:09:22 +0000
@@ -0,0 +1,296 @@
+package main
+
+import (
+ "bytes"
+ "io/ioutil"
+ . "launchpad.net/gocheck"
+ "launchpad.net/juju-core/cmd"
+ "launchpad.net/juju-core/environs"
+ "launchpad.net/juju-core/environs/dummy"
+ "launchpad.net/juju-core/state"
+ "launchpad.net/juju-core/testing"
+ "launchpad.net/juju-core/version"
+ "os"
+ "sort"
+)
+
+type syncToolsSuite struct {
+ testing.LoggingSuite
+ home *testing.FakeHome
+}
+
+func (s *syncToolsSuite) SetUpTest(c *C) {
+ s.LoggingSuite.SetUpTest(c)
+ s.home = testing.MakeEmptyFakeHome(c)
+}
+
+func (s *syncToolsSuite) TearDownTest(c *C) {
+ dummy.Reset()
+ s.home.Restore()
+ s.LoggingSuite.TearDownTest(c)
+}
+
+var _ = Suite(&syncToolsSuite{})
+
+func runSyncToolsCommand(c *C, args ...string) (*cmd.Context, error) {
+ return testing.RunCommand(c, &SyncToolsCommand{}, args)
+}
+
+func (s *syncToolsSuite) TestHelp(c *C) {
+ ctx, err := runSyncToolsCommand(c, "-h")
+ c.Assert(err, ErrorMatches, "flag: help requested")
+ c.Assert(ctx, IsNil)
+}
+
+func uploadDummyTools(c *C, vers version.Binary, store environs.Storage) {
+ path := environs.ToolsStoragePath(vers)
+ content := bytes.NewBufferString("content\n")
+ err := store.Put(path, content, int64(content.Len()))
+ c.Assert(err, IsNil)
+}
+
+func deletePublicTools(c *C, store environs.Storage) {
+ // Dummy environments always put fake tools, but we don't want it
+ // confusing our state, so we delete them
+ dummyTools, err := store.List("tools/juju")
+ c.Assert(err, IsNil)
+ for _, path := range dummyTools {
+ err = store.Remove(path)
+ c.Assert(err, IsNil)
+ }
+}
+
+func setupDummyEnvironments(c *C) (env environs.Environ, cleanup func()) {
+ dummyAttrs := map[string]interface{}{
+ "name": "test-source",
+ "type": "dummy",
+ "state-server": false,
+ // Note: Without this, you get "no public ssh keys found", which seems
+ // a bit odd for the "dummy" environment
+ "authorized-keys": "I-am-not-a-real-key",
+ }
+ env, err := environs.NewFromAttrs(dummyAttrs)
+ c.Assert(err, IsNil)
+ c.Assert(env, NotNil)
+ store := env.PublicStorage().(environs.Storage)
+ deletePublicTools(c, store)
+ // Upload multiple tools
+ uploadDummyTools(c, t1000precise.Binary, store)
+ uploadDummyTools(c, t1000quantal.Binary, store)
+ uploadDummyTools(c, t1000quantal32.Binary, store)
+ uploadDummyTools(c, t1900quantal.Binary, store)
+ // Overwrite the official source bucket to the new dummy 'test-source',
+ // saving the original value for cleanup
+ orig := officialBucketAttrs
+ officialBucketAttrs = dummyAttrs
+ // Create a target dummy environment
+ c.Assert(os.Mkdir(testing.HomePath(".juju"), 0775), IsNil)
+ jujupath := testing.HomePath(".juju", "environments.yaml")
+ err = ioutil.WriteFile(
+ jujupath,
+ []byte(`
+environments:
+ test-target:
+ type: dummy
+ state-server: false
+ authorized-keys: "not-really-one"
+`),
+ 0660)
+ c.Assert(err, IsNil)
+ return env, func() { officialBucketAttrs = orig }
+}
+
+func assertToolsList(c *C, toolsList []*state.Tools, expected ...string) {
+ sort.Strings(expected)
+ actual := make([]string, len(toolsList))
+ for i, tool := range toolsList {
+ actual[i] = tool.Binary.String()
+ }
+ sort.Strings(actual)
+ // In gocheck, the empty slice does not equal the nil slice, though it
+ // does for our purposes
+ if expected == nil {
+ expected = []string{}
+ }
+ c.Assert(actual, DeepEquals, expected)
+}
+
+func setupTargetEnv(c *C) environs.Environ {
+ targetEnv, err := environs.NewFromName("test-target")
+ c.Assert(err, IsNil)
+ store := targetEnv.PublicStorage().(environs.Storage)
+ deletePublicTools(c, store)
+ targetTools, err := environs.ListTools(targetEnv, 1)
+ // Target has no tools.
+ c.Assert(targetTools.Public, HasLen, 0)
+ c.Assert(targetTools.Private, HasLen, 0)
+ return targetEnv
+}
+
+func (s *syncToolsSuite) TestCopyNewestFromDummy(c *C) {
+ sourceEnv, cleanup := setupDummyEnvironments(c)
+ defer cleanup()
+ sourceTools, err := environs.ListTools(sourceEnv, 1)
+ c.Assert(err, IsNil)
+ assertToolsList(c, sourceTools.Public,
+ "1.0.0-precise-amd64", "1.0.0-quantal-amd64",
+ "1.0.0-quantal-i386", "1.9.0-quantal-amd64")
+ c.Assert(sourceTools.Private, HasLen, 0)
+
+ targetEnv := setupTargetEnv(c)
+
+ ctx, err := runSyncToolsCommand(c, "-e", "test-target")
+ c.Assert(err, IsNil)
+ c.Assert(ctx, NotNil)
+ targetTools, err := environs.ListTools(targetEnv, 1)
+ c.Assert(err, IsNil)
+ // No change to the Public bucket
+ c.Assert(targetTools.Public, HasLen, 0)
+ // only the newest added to the private bucket
+ assertToolsList(c, targetTools.Private, "1.9.0-quantal-amd64")
+}
+
+func (s *syncToolsSuite) TestCopyAllFromDummy(c *C) {
+ sourceEnv, cleanup := setupDummyEnvironments(c)
+ defer cleanup()
+ sourceTools, err := environs.ListTools(sourceEnv, 1)
+ c.Assert(err, IsNil)
+ assertToolsList(c, sourceTools.Public,
+ "1.0.0-precise-amd64", "1.0.0-quantal-amd64",
+ "1.0.0-quantal-i386", "1.9.0-quantal-amd64")
+ c.Assert(sourceTools.Private, HasLen, 0)
+
+ targetEnv := setupTargetEnv(c)
+
+ ctx, err := runSyncToolsCommand(c, "-e", "test-target", "--all")
+ c.Assert(err, IsNil)
+ c.Assert(ctx, NotNil)
+ targetTools, err := environs.ListTools(targetEnv, 1)
+ c.Assert(err, IsNil)
+ // No change to the Public bucket
+ c.Assert(targetTools.Public, HasLen, 0)
+ // all tools added to the private bucket
+ assertToolsList(c, targetTools.Private,
+ "1.0.0-precise-amd64", "1.0.0-quantal-amd64",
+ "1.0.0-quantal-i386", "1.9.0-quantal-amd64")
+}
+
+func (s *syncToolsSuite) TestCopyToDummyPublic(c *C) {
+ sourceEnv, cleanup := setupDummyEnvironments(c)
+ defer cleanup()
+ sourceTools, err := environs.ListTools(sourceEnv, 1)
+ c.Assert(err, IsNil)
+ assertToolsList(c, sourceTools.Public,
+ "1.0.0-precise-amd64", "1.0.0-quantal-amd64",
+ "1.0.0-quantal-i386", "1.9.0-quantal-amd64")
+ c.Assert(sourceTools.Private, HasLen, 0)
+
+ targetEnv := setupTargetEnv(c)
+
+ ctx, err := runSyncToolsCommand(c, "-e", "test-target", "--public")
+ c.Assert(err, IsNil)
+ c.Assert(ctx, NotNil)
+ targetTools, err := environs.ListTools(targetEnv, 1)
+ c.Assert(err, IsNil)
+ // newest tools added to the private bucket
+ assertToolsList(c, targetTools.Public, "1.9.0-quantal-amd64")
+ c.Assert(targetTools.Private, HasLen, 0)
+}
+
+type toolSuite struct{}
+
+var _ = Suite(&toolSuite{})
+
+func mustParseTools(major, minor, patch, build int, series string, arch string) *state.Tools {
+ return &state.Tools{
+ Binary: version.Binary{
+ Number: version.Number{major, minor, patch, build},
+ Series: series,
+ Arch: arch}}
+}
+
+var (
+ t1000precise = mustParseTools(1, 0, 0, 0, "precise", "amd64")
+ t1000quantal = mustParseTools(1, 0, 0, 0, "quantal", "amd64")
+ t1000quantal32 = mustParseTools(1, 0, 0, 0, "quantal", "i386")
+ t1900quantal = mustParseTools(1, 9, 0, 0, "quantal", "amd64")
+ t2000precise = mustParseTools(2, 0, 0, 0, "precise", "amd64")
+)
+
+func (s *toolSuite) TestFindNewestOneTool(c *C) {
+ for i, t := range []*state.Tools{
+ t1000precise,
+ t1000quantal,
+ t1900quantal,
+ t2000precise,
+ } {
+ c.Log("test: %d %s", i, t.Binary.String())
+ toolList := []*state.Tools{t}
+ res := findNewest(toolList)
+ c.Assert(res, HasLen, 1)
+ c.Assert(res[0], Equals, t)
+ }
+}
+
+func (s *toolSuite) TestFindNewestOnlyOneBest(c *C) {
+ res := findNewest([]*state.Tools{t1000precise, t1900quantal})
+ c.Assert(res, HasLen, 1)
+ c.Assert(res[0], Equals, t1900quantal)
+}
+
+func (s *toolSuite) TestFindNewestMultipleBest(c *C) {
+ source := []*state.Tools{t1000precise, t1000quantal}
+ res := findNewest(source)
+ c.Assert(res, HasLen, 2)
+ // Order isn't strictly specified, but findNewest currently returns the
+ // order in source, so it makes the test easier to write
+ c.Assert(res, DeepEquals, source)
+}
+
+func (s *toolSuite) TestFindMissingNoTarget(c *C) {
+ for i, t := range [][]*state.Tools{
+ []*state.Tools{t1000precise},
+ []*state.Tools{t1000precise, t1000quantal},
+ } {
+ c.Log("test: %d", i)
+ res := findMissing(t, []*state.Tools(nil))
+ c.Assert(res, DeepEquals, t)
+ }
+}
+
+func (s *toolSuite) TestFindMissingSameEntries(c *C) {
+ for i, t := range [][]*state.Tools{
+ []*state.Tools{t1000precise},
+ []*state.Tools{t1000precise, t1000quantal},
+ } {
+ c.Log("test: %d", i)
+ res := findMissing(t, t)
+ c.Assert(res, HasLen, 0)
+ }
+}
+
+func (s *toolSuite) TestFindHasVersionNotSeries(c *C) {
+ res := findMissing(
+ []*state.Tools{t1000precise, t1000quantal},
+ []*state.Tools{t1000quantal})
+ c.Assert(res, HasLen, 1)
+ c.Assert(res[0], Equals, t1000precise)
+ res = findMissing(
+ []*state.Tools{t1000precise, t1000quantal},
+ []*state.Tools{t1000precise})
+ c.Assert(res, HasLen, 1)
+ c.Assert(res[0], Equals, t1000quantal)
+}
+
+func (s *toolSuite) TestFindHasDifferentArch(c *C) {
+ res := findMissing(
+ []*state.Tools{t1000quantal, t1000quantal32},
+ []*state.Tools{t1000quantal})
+ c.Assert(res, HasLen, 1)
+ c.Assert(res[0], Equals, t1000quantal32)
+ res = findMissing(
+ []*state.Tools{t1000quantal, t1000quantal32},
+ []*state.Tools{t1000quantal32})
+ c.Assert(res, HasLen, 1)
+ c.Assert(res[0], Equals, t1000quantal)
+}
=== modified file 'cmd/juju/unexpose.go'
--- cmd/juju/unexpose.go 2013-02-28 14:46:04 +0000
+++ cmd/juju/unexpose.go 2013-04-08 08:09:22 +0000
@@ -2,16 +2,15 @@
import (
"errors"
-
- "launchpad.net/gnuflag"
"launchpad.net/juju-core/cmd"
"launchpad.net/juju-core/juju"
+ "launchpad.net/juju-core/state/api/params"
"launchpad.net/juju-core/state/statecmd"
)
// UnexposeCommand is responsible exposing services.
type UnexposeCommand struct {
- EnvName string
+ EnvCommandBase
ServiceName string
}
@@ -23,10 +22,6 @@
}
}
-func (c *UnexposeCommand) SetFlags(f *gnuflag.FlagSet) {
- addEnvironFlags(&c.EnvName, f)
-}
-
func (c *UnexposeCommand) Init(args []string) error {
if len(args) == 0 {
return errors.New("no service name specified")
@@ -43,6 +38,6 @@
return err
}
defer conn.Close()
- params := statecmd.ServiceUnexposeParams{ServiceName: c.ServiceName}
+ params := params.ServiceUnexpose{ServiceName: c.ServiceName}
return statecmd.ServiceUnexpose(conn.State, params)
}
=== added file 'cmd/juju/upgradecharm.go'
--- cmd/juju/upgradecharm.go 1970-01-01 00:00:00 +0000
+++ cmd/juju/upgradecharm.go 2013-04-08 08:09:22 +0000
@@ -0,0 +1,110 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "launchpad.net/gnuflag"
+ "launchpad.net/juju-core/charm"
+ "launchpad.net/juju-core/cmd"
+ "launchpad.net/juju-core/juju"
+ "launchpad.net/juju-core/state"
+ "os"
+)
+
+// UpgradeCharm is responsible for upgrading a service's charm.
+type UpgradeCharmCommand struct {
+ EnvCommandBase
+ ServiceName string
+ Force bool
+ RepoPath string // defaults to JUJU_REPOSITORY
+}
+
+const upgradeCharmDoc = `
+When no flags are set, the service's charm will be upgraded to the latest
+revision available in the repository from which it was originally deployed.
+
+If the charm came from a local repository, its path will be assumed to be
+$JUJU_REPOSITORY unless overridden by --repository. If there is no newer
+revision of a local charm directory, the local directory's revision will be
+automatically incremented to create a newer charm.
+
+The local repository behaviour is tuned specifically to the workflow of a charm
+author working on a single client machine; use of local repositories from
+multiple clients is not supported and may lead to confusing behaviour.
+
+Use of the --force flag is not generally recommended; units upgraded while in
+an error state will not have upgrade-charm hooks executed, and may cause
+unexpected behavior.
+`
+
+func (c *UpgradeCharmCommand) Info() *cmd.Info {
+ return &cmd.Info{
+ Name: "upgrade-charm",
+ Args: "<service>",
+ Purpose: "upgrade a service's charm",
+ Doc: upgradeCharmDoc,
+ }
+}
+
+func (c *UpgradeCharmCommand) SetFlags(f *gnuflag.FlagSet) {
+ c.EnvCommandBase.SetFlags(f)
+ f.BoolVar(&c.Force, "force", false, "upgrade all units immediately, even if in error state")
+ f.StringVar(&c.RepoPath, "repository", os.Getenv("JUJU_REPOSITORY"), "local charm repository path")
+}
+
+func (c *UpgradeCharmCommand) Init(args []string) error {
+ switch len(args) {
+ case 1:
+ if !state.IsServiceName(args[0]) {
+ return fmt.Errorf("invalid service name %q", args[0])
+ }
+ c.ServiceName = args[0]
+ case 0:
+ return errors.New("no service specified")
+ default:
+ return cmd.CheckEmpty(args[1:])
+ }
+ // TODO(dimitern): add the other flags --switch and --revision.
+ return nil
+}
+
+// Run connects to the specified environment and starts the charm
+// upgrade process.
+func (c *UpgradeCharmCommand) Run(ctx *cmd.Context) error {
+ conn, err := juju.NewConnFromName(c.EnvName)
+ if err != nil {
+ return err
+ }
+ defer conn.Close()
+ service, err := conn.State.Service(c.ServiceName)
+ if err != nil {
+ return err
+ }
+ curl, _ := service.CharmURL()
+ repo, err := charm.InferRepository(curl, ctx.AbsPath(c.RepoPath))
+ if err != nil {
+ return err
+ }
+ rev, err := repo.Latest(curl)
+ if err != nil {
+ return err
+ }
+ bumpRevision := false
+ if curl.Revision == rev {
+ if _, isLocal := repo.(*charm.LocalRepository); !isLocal {
+ return fmt.Errorf("already running latest charm %q", curl)
+ }
+ // This is a local repository.
+ if ch, err := repo.Get(curl); err != nil {
+ return err
+ } else if _, bumpRevision = ch.(*charm.Dir); !bumpRevision {
+ // Only bump the revision when it's a directory.
+ return fmt.Errorf("already running latest charm %q", curl)
+ }
+ }
+ sch, err := conn.PutCharm(curl.WithRevision(rev), repo, bumpRevision)
+ if err != nil {
+ return err
+ }
+ return service.SetCharm(sch, c.Force)
+}
=== added file 'cmd/juju/upgradecharm_test.go'
--- cmd/juju/upgradecharm_test.go 1970-01-01 00:00:00 +0000
+++ cmd/juju/upgradecharm_test.go 2013-04-08 08:09:22 +0000
@@ -0,0 +1,140 @@
+package main
+
+import (
+ "bytes"
+ "io/ioutil"
+ . "launchpad.net/gocheck"
+ "launchpad.net/juju-core/charm"
+ "launchpad.net/juju-core/state"
+ "launchpad.net/juju-core/testing"
+ "os"
+ "path"
+)
+
+type UpgradeCharmErrorsSuite struct {
+ repoSuite
+}
+
+var _ = Suite(&UpgradeCharmErrorsSuite{})
+
+func runUpgradeCharm(c *C, args ...string) error {
+ _, err := testing.RunCommand(c, &UpgradeCharmCommand{}, args)
+ return err
+}
+
+func (s *UpgradeCharmErrorsSuite) TestInvalidArgs(c *C) {
+ err := runUpgradeCharm(c)
+ c.Assert(err, ErrorMatches, "no service specified")
+ err = runUpgradeCharm(c, "invalid:name")
+ c.Assert(err, ErrorMatches, `invalid service name "invalid:name"`)
+ err = runUpgradeCharm(c, "foo", "bar")
+ c.Assert(err, ErrorMatches, `unrecognized args: \["bar"\]`)
+}
+
+func (s *UpgradeCharmErrorsSuite) TestWithInvalidRepository(c *C) {
+ testing.Charms.ClonedDirPath(s.seriesPath, "riak")
+ err := runDeploy(c, "local:riak", "riak")
+ c.Assert(err, IsNil)
+
+ err = runUpgradeCharm(c, "riak", "--repository=blah")
+ c.Assert(err, ErrorMatches, `no repository found at ".*blah"`)
+ // Reset JUJU_REPOSITORY explicitly, because repoSuite.SetUpTest
+ // overwrites it (TearDownTest will revert it again).
+ os.Setenv("JUJU_REPOSITORY", "")
+ err = runUpgradeCharm(c, "riak", "--repository=")
+ c.Assert(err, ErrorMatches, `no charms found matching "local:precise/riak" in .*`)
+}
+
+func (s *UpgradeCharmErrorsSuite) TestInvalidService(c *C) {
+ err := runUpgradeCharm(c, "phony")
+ c.Assert(err, ErrorMatches, `service "phony" not found`)
+}
+
+func (s *UpgradeCharmErrorsSuite) TestCannotBumpRevisionWithBundle(c *C) {
+ testing.Charms.BundlePath(s.seriesPath, "riak")
+ err := runDeploy(c, "local:riak", "riak")
+ c.Assert(err, IsNil)
+ err = runUpgradeCharm(c, "riak")
+ c.Assert(err, ErrorMatches, `already running latest charm "local:precise/riak-7"`)
+}
+
+type UpgradeCharmSuccessSuite struct {
+ repoSuite
+ path string
+ riak *state.Service
+}
+
+var _ = Suite(&UpgradeCharmSuccessSuite{})
+
+func (s *UpgradeCharmSuccessSuite) SetUpTest(c *C) {
+ s.repoSuite.SetUpTest(c)
+ s.path = testing.Charms.ClonedDirPath(s.seriesPath, "riak")
+ err := runDeploy(c, "local:riak", "riak")
+ c.Assert(err, IsNil)
+ s.riak, err = s.State.Service("riak")
+ c.Assert(err, IsNil)
+ ch, forced, err := s.riak.Charm()
+ c.Assert(err, IsNil)
+ c.Assert(ch.Revision(), Equals, 7)
+ c.Assert(forced, Equals, false)
+}
+
+func (s *UpgradeCharmSuccessSuite) assertUpgraded(c *C, revision int, forced bool) {
+ err := s.riak.Refresh()
+ c.Assert(err, IsNil)
+ ch, force, err := s.riak.Charm()
+ c.Assert(err, IsNil)
+ c.Assert(ch.Revision(), Equals, revision)
+ c.Assert(force, Equals, forced)
+ s.assertCharmUploaded(c, ch.URL())
+}
+
+func (s *UpgradeCharmSuccessSuite) assertLocalRevision(c *C, revision int) {
+ dir, err := charm.ReadDir(s.path)
+ c.Assert(err, IsNil)
+ c.Assert(dir.Revision(), Equals, revision)
+}
+
+func (s *UpgradeCharmSuccessSuite) TestBumpsRevisionWhenNecessary(c *C) {
+ err := runUpgradeCharm(c, "riak")
+ c.Assert(err, IsNil)
+ s.assertUpgraded(c, 8, false)
+ s.assertLocalRevision(c, 8)
+}
+
+func (s *UpgradeCharmSuccessSuite) TestDoesntBumpRevisionWhenNotNecessary(c *C) {
+ dir, err := charm.ReadDir(s.path)
+ c.Assert(err, IsNil)
+ err = dir.SetDiskRevision(42)
+ c.Assert(err, IsNil)
+
+ err = runUpgradeCharm(c, "riak")
+ c.Assert(err, IsNil)
+ s.assertUpgraded(c, 42, false)
+ s.assertLocalRevision(c, 42)
+}
+
+func (s *UpgradeCharmSuccessSuite) TestUpgradesWithBundle(c *C) {
+ dir, err := charm.ReadDir(s.path)
+ c.Assert(err, IsNil)
+ dir.SetRevision(42)
+ buf := &bytes.Buffer{}
+ err = dir.BundleTo(buf)
+ c.Assert(err, IsNil)
+ bundlePath := path.Join(s.seriesPath, "riak.charm")
+ err = ioutil.WriteFile(bundlePath, buf.Bytes(), 0644)
+ c.Assert(err, IsNil)
+ c.Logf("%q %q", bundlePath, s.seriesPath)
+
+ err = runUpgradeCharm(c, "riak")
+ c.Assert(err, IsNil)
+ s.assertUpgraded(c, 42, false)
+ s.assertLocalRevision(c, 7)
+}
+
+func (s *UpgradeCharmSuccessSuite) TestForcedUpgrade(c *C) {
+ err := runUpgradeCharm(c, "riak", "--force")
+ c.Assert(err, IsNil)
+ s.assertUpgraded(c, 8, true)
+ s.assertLocalRevision(c, 8)
+}
=== modified file 'cmd/juju/upgradejuju.go'
--- cmd/juju/upgradejuju.go 2013-02-20 22:11:47 +0000
+++ cmd/juju/upgradejuju.go 2013-04-08 08:09:22 +0000
@@ -13,7 +13,7 @@
// UpgradeJujuCommand upgrades the agents in a juju installation.
type UpgradeJujuCommand struct {
- EnvName string
+ EnvCommandBase
UploadTools bool
BumpVersion bool
Version version.Number
@@ -34,7 +34,7 @@
}
func (c *UpgradeJujuCommand) SetFlags(f *gnuflag.FlagSet) {
- addEnvironFlags(&c.EnvName, f)
+ c.EnvCommandBase.SetFlags(f)
f.BoolVar(&c.UploadTools, "upload-tools", false, "upload local version of tools")
f.StringVar(&c.vers, "version", "", "version to upgrade to (defaults to highest available version with the current major version number)")
f.BoolVar(&c.BumpVersion, "bump-version", false, "upload the tools with a higher build number if necessary, and use that version (overrides --version)")
=== modified file 'cmd/juju/upgradejuju_test.go'
--- cmd/juju/upgradejuju_test.go 2013-02-28 12:27:09 +0000
+++ cmd/juju/upgradejuju_test.go 2013-04-08 08:09:22 +0000
@@ -1,6 +1,7 @@
package main
import (
+ "fmt"
"io/ioutil"
. "launchpad.net/gocheck"
"launchpad.net/juju-core/environs"
@@ -146,11 +147,14 @@
// - we can make it return a tools with a version == version.Current.
// - we don't need to actually rebuild the juju source for each test
// that uses --upload-tools.
-func testPutTools(storage environs.Storage, forceVersion *version.Number) (*state.Tools, error) {
+func testPutTools(storage environs.Storage, forceVersion *version.Number, fakeSeries ...string) (*state.Tools, error) {
vers := version.Current
if forceVersion != nil {
vers.Number = *forceVersion
}
+ if len(fakeSeries) != 0 {
+ return nil, fmt.Errorf("test framework should not be trusted with this")
+ }
upload(storage, vers.String())
return &state.Tools{
Binary: vers,
@@ -166,7 +170,7 @@
}()
for i, test := range upgradeJujuTests {
- c.Logf("%d. %s", i, test.about)
+ c.Logf("\ntest %d: %s", i, test.about)
// Set up the test preconditions.
s.Reset(c)
for _, v := range test.private {
=== modified file 'cmd/jujud/agent.go'
--- cmd/jujud/agent.go 2013-01-22 14:37:30 +0000
+++ cmd/jujud/agent.go 2013-04-08 08:09:22 +0000
@@ -37,9 +37,9 @@
return cmd.CheckEmpty(args)
}
-func (c *AgentConf) read(entityName string) error {
+func (c *AgentConf) read(tag string) error {
var err error
- c.Conf, err = agent.ReadConf(c.dataDir, entityName)
+ c.Conf, err = agent.ReadConf(c.dataDir, tag)
return err
}
@@ -71,7 +71,7 @@
select {
case info := <-done:
if info.err != nil {
- log.Printf("cmd/jujud: %s: %v", tasks[info.index], info.err)
+ log.Errorf("cmd/jujud: %s: %v", tasks[info.index], info.err)
logged[info.index] = true
err = info.err
break waiting
@@ -85,7 +85,7 @@
for i, t := range tasks {
err1 := t.Stop()
if !logged[i] && err1 != nil {
- log.Printf("cmd/jujud: %s: %v", t, err1)
+ log.Errorf("cmd/jujud: %s: %v", t, err1)
logged[i] = true
}
if moreImportant(err1, err) {
@@ -125,31 +125,31 @@
Tomb() *tomb.Tomb
RunOnce(st *state.State, entity AgentState) error
Entity(st *state.State) (AgentState, error)
- EntityName() string
+ Tag() string
}
// runLoop repeatedly calls runOnce until it returns worker.ErrDead or
// an upgraded error, or a value is received on stop.
func runLoop(runOnce func() error, stop <-chan struct{}) error {
- log.Printf("cmd/jujud: agent starting")
+ log.Noticef("cmd/jujud: agent starting")
for {
err := runOnce()
if err == worker.ErrDead {
- log.Printf("cmd/jujud: entity is dead")
+ log.Noticef("cmd/jujud: entity is dead")
return nil
}
if isFatal(err) {
return err
}
if err == nil {
- log.Printf("cmd/jujud: agent died with no error")
+ log.Errorf("cmd/jujud: agent died with no error")
} else {
- log.Printf("cmd/jujud: %v", err)
+ log.Errorf("cmd/jujud: %v", err)
}
if !isleep(retryDelay, stop) {
return nil
}
- log.Printf("cmd/jujud: rerunning agent")
+ log.Noticef("cmd/jujud: rerunning agent")
}
panic("unreachable")
}
@@ -225,7 +225,7 @@
// Ensure we do not lose changes made by another
// worker by re-reading the configuration and changing
// only the password.
- c1, err := agent.ReadConf(c.DataDir, c.EntityName())
+ c1, err := agent.ReadConf(c.DataDir, c.Tag())
if err != nil {
return nil, nil, err
}
@@ -241,24 +241,19 @@
return st, entity, nil
}
-// newDeployManager gives the tests the opportunity to create a deployer.Manager
+// newDeployContext gives the tests the opportunity to create a deployer.Context
// that can be used for testing so as to avoid (1) deploying units to the system
// running the tests and (2) get access to the *State used internally, so that
-// tests can be run without waiting for the 5s watcher refresh time we would
-// otherwise be restricted to. When not testing, st is unused.
-var newDeployManager = func(st *state.State, info *state.Info, dataDir string) deployer.Manager {
- // TODO: pick manager kind based on entity name? (once we have a
- // container manager for prinicpal units, that is; for now, there
+// tests can be run without waiting for the 5s watcher refresh time to which we would
+// otherwise be restricted.
+var newDeployContext = func(st *state.State, dataDir string, deployerName string) deployer.Context {
+ // TODO: pick context kind based on entity name? (once we have a
+ // container context for principal units, that is; for now, there
// is no distinction between principal and subordinate deployments)
- return deployer.NewSimpleManager(info, dataDir)
+ return deployer.NewSimpleContext(dataDir, st.CACert(), deployerName, st)
}
func newDeployer(st *state.State, w *state.UnitsWatcher, dataDir string) *deployer.Deployer {
- info := &state.Info{
- EntityName: w.EntityName(),
- Addrs: st.Addrs(),
- CACert: st.CACert(),
- }
- mgr := newDeployManager(st, info, dataDir)
- return deployer.NewDeployer(st, mgr, w)
+ ctx := newDeployContext(st, dataDir, w.Tag())
+ return deployer.NewDeployer(st, ctx, w)
}
=== modified file 'cmd/jujud/agent_test.go'
--- cmd/jujud/agent_test.go 2013-02-28 12:27:09 +0000
+++ cmd/jujud/agent_test.go 2013-04-08 08:09:22 +0000
@@ -244,7 +244,7 @@
}
type entity interface {
- EntityName() string
+ state.Tagger
SetMongoPassword(string) error
}
@@ -256,9 +256,9 @@
// primeAgent writes the configuration file and tools
// for an agent with the given entity name.
// It returns the agent's configuration and the current tools.
-func (s *agentSuite) primeAgent(c *C, entityName, password string) (*agent.Conf, *state.Tools) {
+func (s *agentSuite) primeAgent(c *C, tag, password string) (*agent.Conf, *state.Tools) {
tools := s.primeTools(c, version.Current)
- tools1, err := agent.ChangeAgentTools(s.DataDir(), entityName, version.Current)
+ tools1, err := agent.ChangeAgentTools(s.DataDir(), tag, version.Current)
c.Assert(err, IsNil)
c.Assert(tools1, DeepEquals, tools)
@@ -267,7 +267,7 @@
OldPassword: password,
StateInfo: s.StateInfo(c),
}
- conf.StateInfo.EntityName = entityName
+ conf.StateInfo.Tag = tag
err = conf.Write()
c.Assert(err, IsNil)
return conf, tools
@@ -321,7 +321,7 @@
}
func (s *agentSuite) testAgentPasswordChanging(c *C, ent entity, newAgent func() runner) {
- conf, err := agent.ReadConf(s.DataDir(), ent.EntityName())
+ conf, err := agent.ReadConf(s.DataDir(), ent.Tag())
c.Assert(err, IsNil)
// Check that it starts initially and changes the password
@@ -340,7 +340,7 @@
// Check that we can no longer gain access with the initial password.
info := s.StateInfo(c)
- info.EntityName = ent.EntityName()
+ info.Tag = ent.Tag()
info.Password = "initial"
testOpenState(c, info, state.Unauthorizedf("unauth"))
@@ -386,7 +386,7 @@
}
func refreshConfig(c *agent.Conf) error {
- nc, err := agent.ReadConf(c.DataDir, c.StateInfo.EntityName)
+ nc, err := agent.ReadConf(c.DataDir, c.StateInfo.Tag)
if err != nil {
return err
}
=== modified file 'cmd/jujud/bootstrap.go'
--- cmd/jujud/bootstrap.go 2013-02-27 13:28:43 +0000
+++ cmd/jujud/bootstrap.go 2013-04-08 08:09:22 +0000
@@ -6,6 +6,7 @@
"launchpad.net/gnuflag"
"launchpad.net/goyaml"
"launchpad.net/juju-core/cmd"
+ "launchpad.net/juju-core/constraints"
"launchpad.net/juju-core/environs"
"launchpad.net/juju-core/environs/config"
"launchpad.net/juju-core/state"
@@ -13,8 +14,10 @@
)
type BootstrapCommand struct {
- Conf AgentConf
- EnvConfig map[string]interface{}
+ cmd.CommandBase
+ Conf AgentConf
+ EnvConfig map[string]interface{}
+ Constraints constraints.Value
}
// Info returns a decription of the command.
@@ -28,6 +31,7 @@
func (c *BootstrapCommand) SetFlags(f *gnuflag.FlagSet) {
c.Conf.addFlags(f)
yamlBase64Var(f, &c.EnvConfig, "env-config", "", "initial environment configuration (yaml, base64 encoded)")
+ f.Var(constraints.ConstraintsValue{&c.Constraints}, "constraints", "initial environment constraints (space-separated strings)")
}
// Init initializes the command for running.
@@ -57,21 +61,27 @@
}
// There is no entity that's created at init time.
- c.Conf.StateInfo.EntityName = ""
- st, err := state.Initialize(c.Conf.StateInfo, cfg)
+ c.Conf.StateInfo.Tag = ""
+ st, err := state.Initialize(c.Conf.StateInfo, cfg, state.DefaultDialOpts())
if err != nil {
return err
}
defer st.Close()
+ if err := st.SetEnvironConstraints(c.Constraints); err != nil {
+ return err
+ }
// TODO: we need to be able to customize machine jobs, not just hardcode these.
- m, err := st.InjectMachine(version.Current.Series, instanceId,
- state.JobManageEnviron, state.JobServeAPI)
+ m, err := st.InjectMachine(
+ version.Current.Series, instanceId,
+ state.JobManageEnviron, state.JobServeAPI,
+ )
if err != nil {
return err
}
- _, err = st.AddUser("admin", c.Conf.OldPassword)
- if err != nil {
+
+ // Set up initial authentication.
+ if _, err := st.AddUser("admin", c.Conf.OldPassword); err != nil {
return err
}
if err := m.SetMongoPassword(c.Conf.OldPassword); err != nil {
=== modified file 'cmd/jujud/bootstrap_test.go'
--- cmd/jujud/bootstrap_test.go 2013-03-04 21:32:22 +0000
+++ cmd/jujud/bootstrap_test.go 2013-04-08 08:09:22 +0000
@@ -4,6 +4,7 @@
"encoding/base64"
. "launchpad.net/gocheck"
"launchpad.net/goyaml"
+ "launchpad.net/juju-core/constraints"
"launchpad.net/juju-core/environs/agent"
"launchpad.net/juju-core/state"
"launchpad.net/juju-core/testing"
@@ -44,9 +45,9 @@
conf := &agent.Conf{
DataDir: s.dataDir,
StateInfo: &state.Info{
- EntityName: "bootstrap",
- Addrs: []string{testing.MgoAddr},
- CACert: []byte(testing.CACert),
+ Tag: "bootstrap",
+ Addrs: []string{testing.MgoAddr},
+ CACert: []byte(testing.CACert),
},
}
err := conf.Write()
@@ -56,17 +57,8 @@
return conf, cmd, err
}
-func (s *BootstrapSuite) TestSetInstanceId(c *C) {
- args := []string{
- "--env-config", b64yaml{
- "name": "dummyenv",
- "type": "dummy",
- "state-server": false,
- "authorized-keys": "i-am-a-key",
- "ca-cert": testing.CACert,
- }.encode(),
- }
- _, cmd, err := s.initBootstrapCommand(c, args...)
+func (s *BootstrapSuite) TestInitializeEnvironment(c *C) {
+ _, cmd, err := s.initBootstrapCommand(c, "--env-config", testConfig)
c.Assert(err, IsNil)
err = cmd.Run(nil)
c.Assert(err, IsNil)
@@ -74,29 +66,53 @@
st, err := state.Open(&state.Info{
Addrs: []string{testing.MgoAddr},
CACert: []byte(testing.CACert),
- })
+ }, state.DefaultDialOpts())
c.Assert(err, IsNil)
defer st.Close()
machines, err := st.AllMachines()
c.Assert(err, IsNil)
- c.Assert(len(machines), Equals, 1)
+ c.Assert(machines, HasLen, 1)
instid, ok := machines[0].InstanceId()
c.Assert(ok, Equals, true)
c.Assert(instid, Equals, state.InstanceId("dummy.instance.id"))
+
+ cons, err := st.EnvironConstraints()
+ c.Assert(err, IsNil)
+ c.Assert(cons, DeepEquals, constraints.Value{})
+}
+
+func (s *BootstrapSuite) TestSetConstraints(c *C) {
+ tcons := constraints.Value{Mem: uint64p(2048), CpuCores: uint64p(2)}
+ _, cmd, err := s.initBootstrapCommand(c, "--env-config", testConfig, "--constraints", tcons.String())
+ c.Assert(err, IsNil)
+ err = cmd.Run(nil)
+ c.Assert(err, IsNil)
+
+ st, err := state.Open(&state.Info{
+ Addrs: []string{testing.MgoAddr},
+ CACert: []byte(testing.CACert),
+ }, state.DefaultDialOpts())
+ c.Assert(err, IsNil)
+ defer st.Close()
+ cons, err := st.EnvironConstraints()
+ c.Assert(err, IsNil)
+ c.Assert(cons, DeepEquals, tcons)
+
+ machines, err := st.AllMachines()
+ c.Assert(err, IsNil)
+ c.Assert(machines, HasLen, 1)
+ cons, err = machines[0].Constraints()
+ c.Assert(err, IsNil)
+ c.Assert(cons, DeepEquals, tcons)
+}
+
+func uint64p(v uint64) *uint64 {
+ return &v
}
func (s *BootstrapSuite) TestMachinerWorkers(c *C) {
- args := []string{
- "--env-config", b64yaml{
- "name": "dummyenv",
- "type": "dummy",
- "state-server": false,
- "authorized-keys": "i-am-a-key",
- "ca-cert": testing.CACert,
- }.encode(),
- }
- _, cmd, err := s.initBootstrapCommand(c, args...)
+ _, cmd, err := s.initBootstrapCommand(c, "--env-config", testConfig)
c.Assert(err, IsNil)
err = cmd.Run(nil)
c.Assert(err, IsNil)
@@ -104,7 +120,7 @@
st, err := state.Open(&state.Info{
Addrs: []string{testing.MgoAddr},
CACert: []byte(testing.CACert),
- })
+ }, state.DefaultDialOpts())
c.Assert(err, IsNil)
defer st.Close()
m, err := st.Machine("0")
@@ -113,7 +129,7 @@
}
func testOpenState(c *C, info *state.Info, expectErr error) {
- st, err := state.Open(info)
+ st, err := state.Open(info, state.DefaultDialOpts())
if st != nil {
st.Close()
}
@@ -125,16 +141,7 @@
}
func (s *BootstrapSuite) TestInitialPassword(c *C) {
- args := []string{
- "--env-config", b64yaml{
- "name": "dummyenv",
- "type": "dummy",
- "state-server": false,
- "authorized-keys": "i-am-a-key",
- "ca-cert": testing.CACert,
- }.encode(),
- }
- conf, cmd, err := s.initBootstrapCommand(c, args...)
+ conf, cmd, err := s.initBootstrapCommand(c, "--env-config", testConfig)
c.Assert(err, IsNil)
conf.OldPassword = "foo"
err = conf.Write()
@@ -151,11 +158,11 @@
}
testOpenState(c, info, state.Unauthorizedf("some auth problem"))
- info.EntityName, info.Password = "machine-0", "foo"
+ info.Tag, info.Password = "machine-0", "foo"
testOpenState(c, info, nil)
- info.EntityName = ""
- st, err := state.Open(info)
+ info.Tag = ""
+ st, err := state.Open(info, state.DefaultDialOpts())
c.Assert(err, IsNil)
defer st.Close()
@@ -216,3 +223,12 @@
}
return base64.StdEncoding.EncodeToString(data)
}
+
+var testConfig = b64yaml{
+ "name": "dummyenv",
+ "type": "dummy",
+ "state-server": false,
+ "authorized-keys": "i-am-a-key",
+ "ca-cert": testing.CACert,
+ "ca-private-key": "",
+}.encode()
=== modified file 'cmd/jujud/deploy_test.go'
--- cmd/jujud/deploy_test.go 2013-01-11 16:23:37 +0000
+++ cmd/jujud/deploy_test.go 2013-04-08 08:09:22 +0000
@@ -16,54 +16,54 @@
// created in the test -- to StartSync and cause the task to actually start
// a sync and observe changes to the set of desired units (and thereby run
// deployment tests in a reasonable amount of time).
-type fakeManager struct {
+type fakeContext struct {
mu sync.Mutex
deployed map[string]bool
st *state.State
inited chan struct{}
}
-func (mgr *fakeManager) DeployUnit(unitName, _ string) error {
- mgr.mu.Lock()
- mgr.deployed[unitName] = true
- mgr.mu.Unlock()
- return nil
-}
-
-func (mgr *fakeManager) RecallUnit(unitName string) error {
- mgr.mu.Lock()
- delete(mgr.deployed, unitName)
- mgr.mu.Unlock()
- return nil
-}
-
-func (mgr *fakeManager) DeployedUnits() ([]string, error) {
+func (ctx *fakeContext) DeployUnit(unitName, _ string) error {
+ ctx.mu.Lock()
+ ctx.deployed[unitName] = true
+ ctx.mu.Unlock()
+ return nil
+}
+
+func (ctx *fakeContext) RecallUnit(unitName string) error {
+ ctx.mu.Lock()
+ delete(ctx.deployed, unitName)
+ ctx.mu.Unlock()
+ return nil
+}
+
+func (ctx *fakeContext) DeployedUnits() ([]string, error) {
var unitNames []string
- mgr.mu.Lock()
- for unitName := range mgr.deployed {
+ ctx.mu.Lock()
+ for unitName := range ctx.deployed {
unitNames = append(unitNames, unitName)
}
- mgr.mu.Unlock()
+ ctx.mu.Unlock()
sort.Strings(unitNames)
return unitNames, nil
}
-func (mgr *fakeManager) waitDeployed(c *C, want ...string) {
+func (ctx *fakeContext) waitDeployed(c *C, want ...string) {
sort.Strings(want)
timeout := time.After(500 * time.Millisecond)
select {
case <-timeout:
c.Fatalf("manager never initialized")
- case <-mgr.inited:
+ case <-ctx.inited:
for {
- mgr.st.StartSync()
+ ctx.st.StartSync()
select {
case <-timeout:
- got, err := mgr.DeployedUnits()
+ got, err := ctx.DeployedUnits()
c.Assert(err, IsNil)
c.Fatalf("unexpected units: %#v", got)
case <-time.After(50 * time.Millisecond):
- got, err := mgr.DeployedUnits()
+ got, err := ctx.DeployedUnits()
c.Assert(err, IsNil)
if reflect.DeepEqual(got, want) {
return
@@ -74,23 +74,22 @@
panic("unreachable")
}
-func patchDeployManager(c *C, expectInfo *state.Info, expectDataDir string) (*fakeManager, func()) {
- mgr := &fakeManager{
+func patchDeployContext(c *C, expectInfo *state.Info, expectDataDir string) (*fakeContext, func()) {
+ ctx := &fakeContext{
deployed: map[string]bool{},
inited: make(chan struct{}),
}
e0 := *expectInfo
expectInfo = &e0
- orig := newDeployManager
- newDeployManager = func(st *state.State, info *state.Info, dataDir string) deployer.Manager {
- c.Check(info.Addrs, DeepEquals, expectInfo.Addrs)
- c.Check(info.CACert, DeepEquals, expectInfo.CACert)
- c.Check(info.EntityName, Equals, expectInfo.EntityName)
- c.Check(info.Password, Equals, "")
+ orig := newDeployContext
+ newDeployContext = func(st *state.State, dataDir string, deployerTag string) deployer.Context {
+ c.Check(st.Addresses(), DeepEquals, expectInfo.Addrs)
+ c.Check(st.CACert(), DeepEquals, expectInfo.CACert)
+ c.Check(deployerTag, Equals, expectInfo.Tag)
c.Check(dataDir, Equals, expectDataDir)
- mgr.st = st
- close(mgr.inited)
- return mgr
+ ctx.st = st
+ close(ctx.inited)
+ return ctx
}
- return mgr, func() { newDeployManager = orig }
+ return ctx, func() { newDeployContext = orig }
}
=== modified file 'cmd/jujud/machine.go'
--- cmd/jujud/machine.go 2013-03-27 07:23:57 +0000
+++ cmd/jujud/machine.go 2013-04-08 08:09:22 +0000
@@ -5,14 +5,12 @@
"launchpad.net/gnuflag"
"launchpad.net/juju-core/cmd"
"launchpad.net/juju-core/environs/agent"
- _ "launchpad.net/juju-core/environs/ec2"
- _ "launchpad.net/juju-core/environs/maas"
- _ "launchpad.net/juju-core/environs/openstack"
"launchpad.net/juju-core/log"
"launchpad.net/juju-core/state"
- "launchpad.net/juju-core/state/api"
+ "launchpad.net/juju-core/state/apiserver"
"launchpad.net/juju-core/worker"
"launchpad.net/juju-core/worker/firewaller"
+ "launchpad.net/juju-core/worker/machiner"
"launchpad.net/juju-core/worker/provisioner"
"launchpad.net/tomb"
"time"
@@ -22,6 +20,7 @@
// MachineAgent is a cmd.Command responsible for running a machine agent.
type MachineAgent struct {
+ cmd.CommandBase
tomb tomb.Tomb
Conf AgentConf
MachineId string
@@ -56,10 +55,10 @@
// Run runs a machine agent.
func (a *MachineAgent) Run(_ *cmd.Context) error {
- if err := a.Conf.read(state.MachineEntityName(a.MachineId)); err != nil {
+ if err := a.Conf.read(state.MachineTag(a.MachineId)); err != nil {
return err
}
- defer log.Printf("cmd/jujud: machine agent exiting")
+ defer log.Noticef("cmd/jujud: machine agent exiting")
defer a.tomb.Done()
// We run the API server worker first, because we may
@@ -103,8 +102,11 @@
func (a *MachineAgent) RunOnce(st *state.State, e AgentState) error {
m := e.(*state.Machine)
- log.Printf("cmd/jujud: jobs for machine agent: %v", m.Jobs())
- tasks := []task{NewUpgrader(st, m, a.Conf.DataDir)}
+ log.Infof("cmd/jujud: jobs for machine agent: %v", m.Jobs())
+ tasks := []task{
+ NewUpgrader(st, m, a.Conf.DataDir),
+ machiner.NewMachiner(st, m.Id()),
+ }
for _, j := range m.Jobs() {
switch j {
case state.JobHostUnits:
@@ -118,7 +120,7 @@
// Ignore because it's started independently.
continue
default:
- log.Printf("cmd/jujud: ignoring unknown job %q", j)
+ log.Warningf("cmd/jujud: ignoring unknown job %q", j)
}
}
return runTasks(a.tomb.Dying(), tasks...)
@@ -128,8 +130,8 @@
return st.Machine(a.MachineId)
}
-func (a *MachineAgent) EntityName() string {
- return state.MachineEntityName(a.MachineId)
+func (a *MachineAgent) Tag() string {
+ return state.MachineTag(a.MachineId)
}
func (a *MachineAgent) Tomb() *tomb.Tomb {
@@ -174,15 +176,15 @@
if len(conf.StateServerCert) == 0 || len(conf.StateServerKey) == 0 {
return &fatalError{"configuration does not have state server cert/key"}
}
- log.Printf("cmd/jujud: running API server job")
- srv, err := api.NewServer(st, fmt.Sprintf(":%d", conf.APIPort), conf.StateServerCert, conf.StateServerKey)
+ log.Infof("cmd/jujud: running API server job")
+ srv, err := apiserver.NewServer(st, fmt.Sprintf(":%d", conf.APIPort), conf.StateServerCert, conf.StateServerKey)
if err != nil {
return err
}
select {
case <-a.tomb.Dying():
case <-srv.Dead():
- log.Printf("jujud: API server has died: %v", srv.Stop())
+ log.Noticef("jujud: API server has died: %v", srv.Stop())
}
return srv.Stop()
}
=== modified file 'cmd/jujud/machine_test.go'
--- cmd/jujud/machine_test.go 2013-02-22 09:42:34 +0000
+++ cmd/jujud/machine_test.go 2013-04-08 08:09:22 +0000
@@ -8,6 +8,7 @@
"launchpad.net/juju-core/environs/dummy"
"launchpad.net/juju-core/state"
"launchpad.net/juju-core/state/api"
+ "launchpad.net/juju-core/state/watcher"
"launchpad.net/juju-core/testing"
"reflect"
"time"
@@ -27,7 +28,7 @@
c.Assert(err, IsNil)
err = m.SetMongoPassword("machine-password")
c.Assert(err, IsNil)
- conf, tools := s.agentSuite.primeAgent(c, state.MachineEntityName(m.Id()), "machine-password")
+ conf, tools := s.agentSuite.primeAgent(c, state.MachineTag(m.Id()), "machine-password")
return m, conf, tools
}
@@ -98,10 +99,36 @@
c.Assert(err, IsNil)
}
+func (s *MachineSuite) TestDyingMachine(c *C) {
+ m, _, _ := s.primeAgent(c, state.JobHostUnits)
+ a := s.newAgent(c, m)
+ done := make(chan error)
+ go func() {
+ done <- a.Run(nil)
+ }()
+ defer func() {
+ c.Check(a.Stop(), IsNil)
+ }()
+ time.Sleep(1 * time.Second)
+ err := m.Destroy()
+ c.Assert(err, IsNil)
+ select {
+ case err := <-done:
+ c.Assert(err, IsNil)
+ case <-time.After(watcher.Period * 5 / 4):
+ // TODO(rog) Fix this so it doesn't wait for so long.
+ // https://bugs.launchpad.net/juju-core/+bug/1163983
+ c.Fatalf("timed out waiting for agent to terminate")
+ }
+ err = m.Refresh()
+ c.Assert(err, IsNil)
+ c.Assert(m.Life(), Equals, state.Dead)
+}
+
func (s *MachineSuite) TestHostUnits(c *C) {
m, conf, _ := s.primeAgent(c, state.JobHostUnits)
a := s.newAgent(c, m)
- mgr, reset := patchDeployManager(c, conf.StateInfo, conf.DataDir)
+ ctx, reset := patchDeployContext(c, conf.StateInfo, conf.DataDir)
defer reset()
go func() { c.Check(a.Run(nil), IsNil) }()
defer func() { c.Check(a.Stop(), IsNil) }()
@@ -112,23 +139,23 @@
c.Assert(err, IsNil)
u1, err := svc.AddUnit()
c.Assert(err, IsNil)
- mgr.waitDeployed(c)
+ ctx.waitDeployed(c)
err = u0.AssignToMachine(m)
c.Assert(err, IsNil)
- mgr.waitDeployed(c, u0.Name())
+ ctx.waitDeployed(c, u0.Name())
err = u0.Destroy()
c.Assert(err, IsNil)
- mgr.waitDeployed(c, u0.Name())
+ ctx.waitDeployed(c, u0.Name())
err = u1.AssignToMachine(m)
c.Assert(err, IsNil)
- mgr.waitDeployed(c, u0.Name(), u1.Name())
+ ctx.waitDeployed(c, u0.Name(), u1.Name())
err = u0.EnsureDead()
c.Assert(err, IsNil)
- mgr.waitDeployed(c, u1.Name())
+ ctx.waitDeployed(c, u1.Name())
err = u0.Refresh()
c.Assert(state.IsNotFound(err), Equals, true)
@@ -203,10 +230,10 @@
func addAPIInfo(conf *agent.Conf, m *state.Machine) {
port := testing.FindTCPPort()
conf.APIInfo = &api.Info{
- Addrs: []string{fmt.Sprintf("localhost:%d", port)},
- CACert: []byte(testing.CACert),
- EntityName: m.EntityName(),
- Password: "unused",
+ Addrs: []string{fmt.Sprintf("localhost:%d", port)},
+ CACert: []byte(testing.CACert),
+ Tag: m.Tag(),
+ Password: "unused",
}
conf.StateServerCert = []byte(testing.ServerCert)
conf.StateServerKey = []byte(testing.ServerKey)
=== modified file 'cmd/jujud/main.go'
--- cmd/jujud/main.go 2013-02-13 13:10:26 +0000
+++ cmd/jujud/main.go 2013-04-08 08:09:22 +0000
@@ -9,6 +9,14 @@
"path/filepath"
)
+// When we import an environment provider implementation
+// here, it will register itself with environs.
+import (
+ _ "launchpad.net/juju-core/environs/ec2"
+ _ "launchpad.net/juju-core/environs/maas"
+ _ "launchpad.net/juju-core/environs/openstack"
+)
+
var jujudDoc = `
juju provides easy, intelligent service orchestration on top of environments
such as OpenStack, Amazon AWS, or bare metal. jujud is a component of juju.
@@ -82,18 +90,21 @@
// Main registers subcommands for the jujud executable, and hands over control
// to the cmd package.
func jujuDMain(args []string) (code int, err error) {
- jujud := &cmd.SuperCommand{Name: "jujud", Doc: jujudDoc, Log: &cmd.Log{}}
+ jujud := cmd.NewSuperCommand(cmd.SuperCommandParams{
+ Name: "jujud",
+ Doc: jujudDoc,
+ Log: &cmd.Log{},
+ })
jujud.Register(&BootstrapCommand{})
jujud.Register(&MachineAgent{})
jujud.Register(&UnitAgent{})
- jujud.Register(&VersionCommand{})
+ jujud.Register(&cmd.VersionCommand{})
code = cmd.Main(jujud, cmd.DefaultContext(), args[1:])
return code, nil
}
-// This function is not redundant with main, because it provides an entry point
+// Main is not redundant with main(), because it provides an entry point
// for testing with arbitrary command line arguments.
-
func Main(args []string) {
var code int = 1
var err error
=== modified file 'cmd/jujud/main_test.go'
--- cmd/jujud/main_test.go 2013-02-27 13:28:43 +0000
+++ cmd/jujud/main_test.go 2013-04-08 08:09:22 +0000
@@ -8,6 +8,7 @@
"launchpad.net/gnuflag"
. "launchpad.net/gocheck"
"launchpad.net/juju-core/cmd"
+ "launchpad.net/juju-core/environs"
"launchpad.net/juju-core/testing"
"launchpad.net/juju-core/worker/uniter/jujuc"
"os"
@@ -63,7 +64,6 @@
func (s *MainSuite) TestParseErrors(c *C) {
// Check all the obvious parse errors
- checkMessage(c, "no command specified")
checkMessage(c, "unrecognized command: jujud cavitate", "cavitate")
msgf := "flag provided but not defined: --cheese"
checkMessage(c, msgf, "--cheese", "cavitate")
@@ -86,7 +86,21 @@
"toastie")
}
+var expectedProviders = []string{
+ "ec2",
+ "openstack",
+}
+
+func (s *MainSuite) TestProvidersAreRegistered(c *C) {
+ // check that all the expected providers are registered
+ for _, name := range expectedProviders {
+ _, err := environs.Provider(name)
+ c.Assert(err, IsNil)
+ }
+}
+
type RemoteCommand struct {
+ cmd.CommandBase
msg string
}
=== modified file 'cmd/jujud/unit.go'
--- cmd/jujud/unit.go 2013-02-20 22:11:47 +0000
+++ cmd/jujud/unit.go 2013-04-08 08:09:22 +0000
@@ -12,6 +12,7 @@
// UnitAgent is a cmd.Command responsible for running a unit agent.
type UnitAgent struct {
+ cmd.CommandBase
tomb tomb.Tomb
Conf AgentConf
UnitName string
@@ -49,10 +50,10 @@
// Run runs a unit agent.
func (a *UnitAgent) Run(ctx *cmd.Context) error {
- if err := a.Conf.read(state.UnitEntityName(a.UnitName)); err != nil {
+ if err := a.Conf.read(state.UnitTag(a.UnitName)); err != nil {
return err
}
- defer log.Printf("cmd/jujud: unit agent exiting")
+ defer log.Noticef("cmd/jujud: unit agent exiting")
defer a.tomb.Done()
err := RunAgentLoop(a.Conf.Conf, a)
if ug, ok := err.(*UpgradeReadyError); ok {
@@ -82,8 +83,8 @@
return st.Unit(a.UnitName)
}
-func (a *UnitAgent) EntityName() string {
- return state.UnitEntityName(a.UnitName)
+func (a *UnitAgent) Tag() string {
+ return state.UnitTag(a.UnitName)
}
func (a *UnitAgent) Tomb() *tomb.Tomb {
=== modified file 'cmd/jujud/unit_test.go'
--- cmd/jujud/unit_test.go 2013-01-25 18:33:16 +0000
+++ cmd/jujud/unit_test.go 2013-04-08 08:09:22 +0000
@@ -5,15 +5,28 @@
"launchpad.net/juju-core/cmd"
"launchpad.net/juju-core/environs/agent"
"launchpad.net/juju-core/state"
+ "launchpad.net/juju-core/state/api/params"
+ "launchpad.net/juju-core/testing"
"time"
)
type UnitSuite struct {
+ testing.GitSuite
agentSuite
}
var _ = Suite(&UnitSuite{})
+func (s *UnitSuite) SetUpTest(c *C) {
+ s.GitSuite.SetUpTest(c)
+ s.agentSuite.SetUpTest(c)
+}
+
+func (s *UnitSuite) TearDownTest(c *C) {
+ s.agentSuite.TearDownTest(c)
+ s.GitSuite.TearDownTest(c)
+}
+
// primeAgent creates a unit, and sets up the unit agent's directory.
// It returns the new unit and the agent's configuration.
func (s *UnitSuite) primeAgent(c *C) (*state.Unit, *agent.Conf, *state.Tools) {
@@ -23,7 +36,7 @@
c.Assert(err, IsNil)
err = unit.SetMongoPassword("unit-password")
c.Assert(err, IsNil)
- conf, tools := s.agentSuite.primeAgent(c, unit.EntityName(), "unit-password")
+ conf, tools := s.agentSuite.primeAgent(c, unit.Tag(), "unit-password")
return unit, conf, tools
}
@@ -70,7 +83,7 @@
func (s *UnitSuite) TestRunStop(c *C) {
unit, conf, _ := s.primeAgent(c)
a := s.newAgent(c, unit)
- mgr, reset := patchDeployManager(c, conf.StateInfo, conf.DataDir)
+ ctx, reset := patchDeployContext(c, conf.StateInfo, conf.DataDir)
defer reset()
go func() { c.Check(a.Run(nil), IsNil) }()
defer func() { c.Check(a.Stop(), IsNil) }()
@@ -87,13 +100,13 @@
st, info, err := unit.Status()
c.Assert(err, IsNil)
switch st {
- case state.UnitPending, state.UnitInstalled:
+ case params.UnitPending, params.UnitInstalled:
c.Logf("waiting...")
continue
- case state.UnitStarted:
+ case params.UnitStarted:
c.Logf("started!")
break waitStarted
- case state.UnitDown:
+ case params.UnitDown:
s.State.StartSync()
c.Logf("unit is still down")
default:
@@ -103,7 +116,7 @@
}
// Check no subordinates have been deployed.
- mgr.waitDeployed(c)
+ ctx.waitDeployed(c)
// Add a relation with a subordinate service and wait for the subordinate
// to be deployed...
@@ -113,14 +126,14 @@
c.Assert(err, IsNil)
_, err = s.State.AddRelation(eps...)
c.Assert(err, IsNil)
- mgr.waitDeployed(c, "logging/0")
+ ctx.waitDeployed(c, "logging/0")
// ...then kill the subordinate and wait for it to be recalled and removed.
logging0, err := s.State.Unit("logging/0")
c.Assert(err, IsNil)
err = logging0.EnsureDead()
c.Assert(err, IsNil)
- mgr.waitDeployed(c)
+ ctx.waitDeployed(c)
err = logging0.Refresh()
c.Assert(state.IsNotFound(err), Equals, true)
}
=== modified file 'cmd/jujud/upgrade.go'
--- cmd/jujud/upgrade.go 2013-02-24 21:51:05 +0000
+++ cmd/jujud/upgrade.go 2013-04-08 08:09:22 +0000
@@ -47,7 +47,7 @@
if err != nil {
return err
}
- log.Printf("cmd/jujud: upgrader upgraded from %v to %v (%q)", e.OldTools.Binary, tools.Binary, tools.URL)
+ log.Infof("cmd/jujud: upgrader upgraded from %v to %v (%q)", e.OldTools.Binary, tools.Binary, tools.URL)
return nil
}
@@ -56,7 +56,7 @@
type AgentState interface {
// SetAgentTools sets the tools that the agent is currently running.
SetAgentTools(tools *state.Tools) error
- EntityName() string
+ Tag() string
SetMongoPassword(password string) error
Life() state.Life
}
@@ -95,7 +95,7 @@
// Don't abort everything because we can't find the tools directory.
// The problem should sort itself out as we will immediately
// download some more tools and upgrade.
- log.Printf("cmd/jujud: upgrader cannot read current tools: %v", err)
+ log.Warningf("cmd/jujud: upgrader cannot read current tools: %v", err)
currentTools = &state.Tools{
Binary: version.Current,
}
@@ -146,13 +146,13 @@
if environ == nil {
environ, err = environs.New(cfg)
if err != nil {
- log.Printf("cmd/jujud: upgrader loaded invalid initial environment configuration: %v", err)
+ log.Errorf("cmd/jujud: upgrader loaded invalid initial environment configuration: %v", err)
break
}
} else {
err = environ.SetConfig(cfg)
if err != nil {
- log.Printf("cmd/jujud: upgrader loaded invalid environment configuration: %v", err)
+ log.Warningf("cmd/jujud: upgrader loaded invalid environment configuration: %v", err)
// continue on, because the version number is still significant.
}
}
@@ -175,18 +175,20 @@
}
binary := version.Current
binary.Number = vers
-
if tools, err := agent.ReadTools(u.dataDir, binary); err == nil {
- // The tools have already been downloaded, so use them.
+ // The exact tools have already been downloaded, so use them.
return u.upgradeReady(currentTools, tools)
}
+
+ // Try to find the proposed tools in the environment, and fall back
+ // to the most recent version no later than the proposed.
flags := environs.CompatVersion
if cfg.Development() {
flags |= environs.DevVersion
}
tools, err := environs.FindTools(environ, binary, flags)
if err != nil {
- log.Printf("cmd/jujud: upgrader error finding tools for %v: %v", binary, err)
+ log.Errorf("cmd/jujud: upgrader error finding tools for %v: %v", binary, err)
noDelay()
// TODO(rog): poll until tools become available.
break
@@ -194,13 +196,18 @@
if tools.Binary != binary {
if tools.Number == version.Current.Number {
// TODO(rog): poll until tools become available.
- log.Printf("cmd/jujud: upgrader: version %v requested but found only current version: %v", binary, tools.Number)
+ log.Warningf("cmd/jujud: upgrader: version %v requested but found only current version: %v", binary, tools.Number)
noDelay()
break
}
- log.Printf("cmd/jujud: upgrader cannot find exact tools match for %s; using %s instead", binary, tools.Binary)
- }
- log.Printf("cmd/jujud: upgrader downloading %q", tools.URL)
+ log.Warningf("cmd/jujud: upgrader cannot find exact tools match for %s; using %s instead", binary, tools.Binary)
+ }
+ if tools, err := agent.ReadTools(u.dataDir, tools.Binary); err == nil {
+ // The best available tools have already been downloaded, so use them.
+ return u.upgradeReady(currentTools, tools)
+ }
+
+ log.Infof("cmd/jujud: upgrader downloading %q", tools.URL)
download = downloader.New(tools.URL, "")
downloadTools = tools
downloadDone = download.Done()
@@ -208,17 +215,17 @@
tools := downloadTools
download, downloadTools, downloadDone = nil, nil, nil
if status.Err != nil {
- log.Printf("cmd/jujud: upgrader download of %v failed: %v", tools.Binary, status.Err)
+ log.Errorf("cmd/jujud: upgrader download of %v failed: %v", tools.Binary, status.Err)
noDelay()
break
}
err := agent.UnpackTools(u.dataDir, tools, status.File)
status.File.Close()
if err := os.Remove(status.File.Name()); err != nil {
- log.Printf("cmd/jujud: upgrader cannot remove temporary download file: %v", err)
+ log.Warningf("cmd/jujud: upgrader cannot remove temporary download file: %v", err)
}
if err != nil {
- log.Printf("cmd/jujud: upgrader cannot unpack %v tools: %v", tools.Binary, err)
+ log.Errorf("cmd/jujud: upgrader cannot unpack %v tools: %v", tools.Binary, err)
noDelay()
break
}
@@ -235,7 +242,7 @@
func (u *Upgrader) upgradeReady(old, new *state.Tools) *UpgradeReadyError {
return &UpgradeReadyError{
- AgentName: u.agentState.EntityName(),
+ AgentName: u.agentState.Tag(),
OldTools: old,
DataDir: u.dataDir,
NewTools: new,
=== modified file 'cmd/jujud/upgrade_test.go'
--- cmd/jujud/upgrade_test.go 2013-02-25 16:46:55 +0000
+++ cmd/jujud/upgrade_test.go 2013-04-08 08:09:22 +0000
@@ -307,7 +307,7 @@
return nil
}
-func (as testAgentState) EntityName() string {
+func (as testAgentState) Tag() string {
return "testagent"
}
=== modified file 'cmd/logging.go'
--- cmd/logging.go 2012-06-21 20:40:39 +0000
+++ cmd/logging.go 2013-04-08 08:09:22 +0000
@@ -6,41 +6,52 @@
"launchpad.net/juju-core/log"
stdlog "log"
"os"
+ "strings"
)
// Log supplies the necessary functionality for Commands that wish to set up
// logging.
type Log struct {
+ Prefix string
Path string
Verbose bool
Debug bool
+ log.Logger
}
// AddFlags adds appropriate flags to f.
-func (c *Log) AddFlags(f *gnuflag.FlagSet) {
- f.StringVar(&c.Path, "log-file", "", "path to write log to")
- f.BoolVar(&c.Verbose, "v", false, "if set, log additional messages")
- f.BoolVar(&c.Verbose, "verbose", false, "if set, log additional messages")
- f.BoolVar(&c.Debug, "debug", false, "if set, log debugging messages")
+func (l *Log) AddFlags(f *gnuflag.FlagSet) {
+ f.StringVar(&l.Path, "log-file", "", "path to write log to")
+ f.BoolVar(&l.Verbose, "v", false, "if set, log additional messages")
+ f.BoolVar(&l.Verbose, "verbose", false, "if set, log additional messages")
+ f.BoolVar(&l.Debug, "debug", false, "if set, log debugging messages")
+}
+
+func (l *Log) Output(calldepth int, s string) error {
+ // split the log line between the LEVEL and the message
+ output := strings.SplitN(s, " ", 2)
+ // recombine it inserting our prefix between the LEVEL and the message
+ output = []string{output[0], l.Prefix, output[1]}
+ return l.Logger.Output(calldepth, strings.Join(output, " "))
}
// Start starts logging using the given Context.
-func (c *Log) Start(ctx *Context) (err error) {
- log.Debug = c.Debug
+func (l *Log) Start(ctx *Context) (err error) {
+ log.Debug = l.Debug
var target io.Writer
- if c.Path != "" {
- path := ctx.AbsPath(c.Path)
+ if l.Path != "" {
+ path := ctx.AbsPath(l.Path)
target, err = os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
if err != nil {
return
}
- } else if c.Verbose || c.Debug {
+ } else if l.Verbose || l.Debug {
target = ctx.Stderr
}
if target != nil {
- log.Target = stdlog.New(target, "", stdlog.LstdFlags)
- } else {
- log.Target = nil
+ l.Prefix = "JUJU:" + l.Prefix
+ l.Logger = stdlog.New(target, "", stdlog.LstdFlags)
+ log.SetTarget(l)
}
return
}
=== modified file 'cmd/logging_test.go'
--- cmd/logging_test.go 2013-02-28 12:27:09 +0000
+++ cmd/logging_test.go 2013-04-08 08:09:22 +0000
@@ -10,22 +10,11 @@
)
type LogSuite struct {
- restoreLog func()
+ testing.LoggingSuite
}
var _ = Suite(&LogSuite{})
-func (s *LogSuite) SetUpTest(c *C) {
- target, debug := log.Target, log.Debug
- s.restoreLog = func() {
- log.Target, log.Debug = target, debug
- }
-}
-
-func (s *LogSuite) TearDownTest(c *C) {
- s.restoreLog()
-}
-
func (s *LogSuite) TestAddFlags(c *C) {
l := &cmd.Log{}
f := testing.NewFlagSet()
@@ -49,7 +38,7 @@
path string
verbose bool
debug bool
- target Checker
+ check Checker
}{
{"", true, true, NotNil},
{"", true, false, NotNil},
@@ -60,45 +49,48 @@
{"foo", false, true, NotNil},
{"foo", false, false, NotNil},
} {
- l := &cmd.Log{t.path, t.verbose, t.debug}
+ // commands always start with the log target set to its zero value.
+ log.SetTarget(nil)
+
+ l := &cmd.Log{Prefix: "test", Path: t.path, Verbose: t.verbose, Debug: t.debug}
ctx := testing.Context(c)
err := l.Start(ctx)
c.Assert(err, IsNil)
- c.Assert(log.Target, t.target)
+ c.Assert(log.Target(), t.check)
c.Assert(log.Debug, Equals, t.debug)
}
}
func (s *LogSuite) TestStderr(c *C) {
- l := &cmd.Log{Verbose: true}
+ l := &cmd.Log{Prefix: "test", Verbose: true}
ctx := testing.Context(c)
err := l.Start(ctx)
c.Assert(err, IsNil)
- log.Printf("hello")
- c.Assert(bufferString(ctx.Stderr), Matches, `.* JUJU hello\n`)
+ log.Infof("hello")
+ c.Assert(bufferString(ctx.Stderr), Matches, `^.* INFO JUJU:test hello\n`)
}
func (s *LogSuite) TestRelPathLog(c *C) {
- l := &cmd.Log{Path: "foo.log"}
+ l := &cmd.Log{Prefix: "test", Path: "foo.log"}
ctx := testing.Context(c)
err := l.Start(ctx)
c.Assert(err, IsNil)
- log.Printf("hello")
+ log.Infof("hello")
c.Assert(bufferString(ctx.Stderr), Equals, "")
content, err := ioutil.ReadFile(filepath.Join(ctx.Dir, "foo.log"))
c.Assert(err, IsNil)
- c.Assert(string(content), Matches, `.* JUJU hello\n`)
+ c.Assert(string(content), Matches, `^.* INFO JUJU:test hello\n`)
}
func (s *LogSuite) TestAbsPathLog(c *C) {
path := filepath.Join(c.MkDir(), "foo.log")
- l := &cmd.Log{Path: path}
+ l := &cmd.Log{Prefix: "test", Path: path}
ctx := testing.Context(c)
err := l.Start(ctx)
c.Assert(err, IsNil)
- log.Printf("hello")
+ log.Infof("hello")
c.Assert(bufferString(ctx.Stderr), Equals, "")
content, err := ioutil.ReadFile(path)
c.Assert(err, IsNil)
- c.Assert(string(content), Matches, `.* JUJU hello\n`)
+ c.Assert(string(content), Matches, `^.* INFO JUJU:test hello\n`)
}
=== modified file 'cmd/output.go'
--- cmd/output.go 2012-07-31 18:50:23 +0000
+++ cmd/output.go 2013-04-08 08:09:22 +0000
@@ -38,7 +38,7 @@
// FormatSmart marshals value into a []byte according to the following rules:
// * string: untouched
-// * bool: converted to `true` or `false`
+// * bool: converted to `True` or `False` (to match pyjuju)
// * int or float: converted to sensible strings
// * []string: joined by `\n`s into a single string
// * anything else: delegate to FormatYaml
@@ -54,7 +54,12 @@
if v.Type().Elem().Kind() == reflect.String {
return []byte(strings.Join(value.([]string), "\n")), nil
}
- case reflect.Map, reflect.Bool, reflect.Float32, reflect.Float64:
+ case reflect.Bool:
+ if value.(bool) {
+ return []byte("True"), nil
+ }
+ return []byte("False"), nil
+ case reflect.Map, reflect.Float32, reflect.Float64:
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
default:
=== modified file 'cmd/output_test.go'
--- cmd/output_test.go 2013-02-28 12:27:09 +0000
+++ cmd/output_test.go 2013-04-08 08:09:22 +0000
@@ -9,6 +9,7 @@
// OutputCommand is a command that uses the output.go formatters.
type OutputCommand struct {
+ cmd.CommandBase
out cmd.Output
value interface{}
}
@@ -48,8 +49,8 @@
{1, "1\n"},
{-1, "-1\n"},
{1.1, "1.1\n"},
- {true, "true\n"},
- {false, "false\n"},
+ {true, "True\n"},
+ {false, "False\n"},
{"hello", "hello\n"},
{"\n\n\n", "\n\n\n\n"},
{"foo: bar", "foo: bar\n"},
@@ -60,8 +61,8 @@
{1, "1\n"},
{-1, "-1\n"},
{1.1, "1.1\n"},
- {true, "true\n"},
- {false, "false\n"},
+ {true, "True\n"},
+ {false, "False\n"},
{"hello", "hello\n"},
{"\n\n\n", "\n\n\n\n"},
{"foo: bar", "foo: bar\n"},
=== modified file 'cmd/supercommand.go'
--- cmd/supercommand.go 2013-02-21 02:17:34 +0000
+++ cmd/supercommand.go 2013-04-08 08:09:22 +0000
@@ -1,32 +1,77 @@
package cmd
import (
+ "bytes"
"fmt"
"launchpad.net/gnuflag"
"sort"
"strings"
)
+type topic struct {
+ short string
+ long func() string
+}
+
+// SuperCommandParams provides a way to have default parameter to the
+// `NewSuperCommand` call.
+type SuperCommandParams struct {
+ Name string
+ Purpose string
+ Doc string
+ Log *Log
+}
+
+// NewSuperCommand creates and initializes a new `SuperCommand`, and returns
+// the fully initialized structure.
+func NewSuperCommand(params SuperCommandParams) *SuperCommand {
+ command := &SuperCommand{
+ Name: params.Name,
+ Purpose: params.Purpose,
+ Doc: params.Doc,
+ Log: params.Log}
+ command.init()
+ return command
+}
+
// SuperCommand is a Command that selects a subcommand and assumes its
// properties; any command line arguments that were not used in selecting
// the subcommand are passed down to it, and to Run a SuperCommand is to run
// its selected subcommand.
type SuperCommand struct {
- Name string
- Purpose string
- Doc string
- Log *Log
- subcmds map[string]Command
- flags *gnuflag.FlagSet
- subcmd Command
+ CommandBase
+ Name string
+ Purpose string
+ Doc string
+ Log *Log
+ subcmds map[string]Command
+ flags *gnuflag.FlagSet
+ subcmd Command
+ showHelp bool
+}
+
+// Because Go doesn't have constructors that initialize the object into a
+// ready state.
+func (c *SuperCommand) init() {
+ if c.subcmds != nil {
+ return
+ }
+ help := &helpCommand{
+ super: c,
+ }
+ help.init()
+ c.subcmds = map[string]Command{
+ "help": help,
+ }
+}
+
+func (c *SuperCommand) AddHelpTopic(name, short, long string) {
+ c.subcmds["help"].(*helpCommand).addTopic(name, short, long)
}
// Register makes a subcommand available for use on the command line. The
// command will be available via its own name, and via any supplied aliases.
func (c *SuperCommand) Register(subcmd Command) {
- if c.subcmds == nil {
- c.subcmds = make(map[string]Command)
- }
info := subcmd.Info()
c.insert(info.Name, subcmd)
for _, name := range info.Aliases {
@@ -35,18 +80,21 @@
}
func (c *SuperCommand) insert(name string, subcmd Command) {
- if _, found := c.subcmds[name]; found {
+ if _, found := c.subcmds[name]; found || name == "help" {
panic(fmt.Sprintf("command already registered: %s", name))
}
c.subcmds[name] = subcmd
}
// describeCommands returns a short description of each registered subcommand.
-func (c *SuperCommand) describeCommands() string {
+func (c *SuperCommand) describeCommands(simple bool) string {
+ var lineFormat = " %-*s - %s"
+ var outputFormat = "commands:\n%s"
+ if simple {
+ lineFormat = "%-*s %s"
+ outputFormat = "%s"
+ }
cmds := make([]string, len(c.subcmds))
- if len(cmds) == 0 {
- return ""
- }
i := 0
longest := 0
for name := range c.subcmds {
@@ -63,9 +111,9 @@
if name != info.Name {
purpose = "alias for " + info.Name
}
- cmds[i] = fmt.Sprintf(" %-*s - %s", longest, name, purpose)
+ cmds[i] = fmt.Sprintf(lineFormat, longest, name, purpose)
}
- return fmt.Sprintf("commands:\n%s", strings.Join(cmds, "\n"))
+ return fmt.Sprintf(outputFormat, strings.Join(cmds, "\n"))
}
// Info returns a description of the currently selected subcommand, or of the
@@ -80,7 +128,7 @@
if doc := strings.TrimSpace(c.Doc); doc != "" {
docParts = append(docParts, doc)
}
- if cmds := c.describeCommands(); cmds != "" {
+ if cmds := c.describeCommands(false); cmds != "" {
docParts = append(docParts, cmds)
}
return &Info{
@@ -91,41 +139,184 @@
}
}
+const helpPurpose = "show help on a command or other topic"
+
// SetFlags adds the options that apply to all commands, particularly those
// due to logging.
func (c *SuperCommand) SetFlags(f *gnuflag.FlagSet) {
if c.Log != nil {
c.Log.AddFlags(f)
}
+ f.BoolVar(&c.showHelp, "h", false, helpPurpose)
+ f.BoolVar(&c.showHelp, "help", false, "")
+
c.flags = f
}
// Init initializes the command for running.
func (c *SuperCommand) Init(args []string) error {
if len(args) == 0 {
- return fmt.Errorf("no command specified")
+ c.subcmd = c.subcmds["help"]
+ return nil
}
+
found := false
+ // Look for the command.
if c.subcmd, found = c.subcmds[args[0]]; !found {
return fmt.Errorf("unrecognized command: %s %s", c.Info().Name, args[0])
}
+ args = args[1:]
c.subcmd.SetFlags(c.flags)
if err := c.flags.Parse(true, args); err != nil {
return err
}
- subargs := c.flags.Args()
- return c.subcmd.Init(subargs[1:])
+ args = c.flags.Args()
+ if c.showHelp {
+ // We want to treat help for the command the same way we would if we went "help foo".
+ args = []string{c.subcmd.Info().Name}
+ c.subcmd = c.subcmds["help"]
+ }
+ return c.subcmd.Init(args)
}
// Run executes the subcommand that was selected in Init.
func (c *SuperCommand) Run(ctx *Context) error {
+ if c.subcmd == nil {
+ panic("Run: missing subcommand; Init failed or not called")
+ }
if c.Log != nil {
+ if c.Log.Prefix == "" {
+ c.Log.Prefix = c.Name + ":" + c.subcmd.Info().Name
+ }
if err := c.Log.Start(ctx); err != nil {
return err
}
}
- if c.subcmd == nil {
- panic("Run: missing subcommand; Init failed or not called")
- }
return c.subcmd.Run(ctx)
}
+
+type helpCommand struct {
+ CommandBase
+ super *SuperCommand
+ topic string
+ topics map[string]topic
+}
+
+func (c *helpCommand) init() {
+ c.topics = map[string]topic{
+ "commands": {
+ short: "Basic help for all commands",
+ long: func() string { return c.super.describeCommands(true) },
+ },
+ "global-options": {
+ short: "Options common to all commands",
+ long: func() string { return c.globalOptions() },
+ },
+ "topics": {
+ short: "Topic list",
+ long: func() string { return c.topicList() },
+ },
+ }
+}
+
+func echo(s string) func() string {
+ return func() string { return s }
+}
+
+func (c *helpCommand) addTopic(name, short, long string) {
+ if _, found := c.topics[name]; found {
+ panic(fmt.Sprintf("help topic already added: %s", name))
+ }
+ c.topics[name] = topic{short, echo(long)}
+}
+
+func (c *helpCommand) globalOptions() string {
+ buf := &bytes.Buffer{}
+ fmt.Fprintf(buf, `Global Options
+
+These options may be used with any command, and may appear in front of any
+command.
+
+`)
+
+ f := gnuflag.NewFlagSet("", gnuflag.ContinueOnError)
+ c.super.SetFlags(f)
+ f.SetOutput(buf)
+ f.PrintDefaults()
+ return buf.String()
+}
+
+func (c *helpCommand) topicList() string {
+ topics := make([]string, len(c.topics))
+ i := 0
+ longest := 0
+ for name := range c.topics {
+ if len(name) > longest {
+ longest = len(name)
+ }
+ topics[i] = name
+ i++
+ }
+ sort.Strings(topics)
+ for i, name := range topics {
+ shortHelp := c.topics[name].short
+ topics[i] = fmt.Sprintf("%-*s %s", longest, name, shortHelp)
+ }
+ return fmt.Sprintf("%s", strings.Join(topics, "\n"))
+}
+
+func (c *helpCommand) Info() *Info {
+ return &Info{
+ Name: "help",
+ Args: "[topic]",
+ Purpose: helpPurpose,
+ Doc: `
+See also: topics
+`,
+ }
+}
+
+func (c *helpCommand) Init(args []string) error {
+ switch len(args) {
+ case 0:
+ case 1:
+ c.topic = args[0]
+ default:
+ return fmt.Errorf("extra arguments to command help: %q", args[2:])
+ }
+ return nil
+}
+
+func (c *helpCommand) Run(ctx *Context) error {
+ // If there is no help topic specified, print basic usage.
+ if c.topic == "" {
+ if _, ok := c.topics["basics"]; ok {
+ c.topic = "basics"
+ } else {
+ // At this point, "help" is selected as the SuperCommand's
+ // sub-command, but we want the info to be printed
+ // as if there was nothing selected.
+ c.super.subcmd = nil
+
+ info := c.super.Info()
+ f := gnuflag.NewFlagSet(info.Name, gnuflag.ContinueOnError)
+ c.SetFlags(f)
+ ctx.Stdout.Write(info.Help(f))
+ return nil
+ }
+ }
+ if helpcmd, ok := c.super.subcmds[c.topic]; ok {
+ info := helpcmd.Info()
+ info.Name = fmt.Sprintf("%s %s", c.super.Name, info.Name)
+ f := gnuflag.NewFlagSet(info.Name, gnuflag.ContinueOnError)
+ helpcmd.SetFlags(f)
+ ctx.Stdout.Write(info.Help(f))
+ return nil
+ }
+ topic, ok := c.topics[c.topic]
+ if !ok {
+ return fmt.Errorf("unknown command or topic for %s", c.topic)
+ }
+ fmt.Fprintf(ctx.Stdout, "%s\n", strings.TrimSpace(topic.long()))
+ return nil
+}
=== modified file 'cmd/supercommand_test.go'
--- cmd/supercommand_test.go 2013-02-28 12:27:09 +0000
+++ cmd/supercommand_test.go 2013-04-08 08:09:22 +0000
@@ -8,7 +8,7 @@
)
func initDefenestrate(args []string) (*cmd.SuperCommand, *TestCommand, error) {
- jc := &cmd.SuperCommand{Name: "jujutest"}
+ jc := cmd.NewSuperCommand(cmd.SuperCommandParams{Name: "jujutest"})
tc := &TestCommand{Name: "defenestrate"}
jc.Register(tc)
return jc, tc, testing.InitCommand(jc, args)
@@ -18,21 +18,22 @@
var _ = Suite(&SuperCommandSuite{})
+const helpText = "\n help\\s+- show help on a command or other topic"
+const helpCommandsText = "commands:" + helpText
+
func (s *SuperCommandSuite) TestDispatch(c *C) {
- jc := &cmd.SuperCommand{Name: "jujutest"}
- err := testing.InitCommand(jc, []string{})
- c.Assert(err, ErrorMatches, `no command specified`)
+ jc := cmd.NewSuperCommand(cmd.SuperCommandParams{Name: "jujutest"})
info := jc.Info()
c.Assert(info.Name, Equals, "jujutest")
c.Assert(info.Args, Equals, "<command> ...")
- c.Assert(info.Doc, Equals, "")
+ c.Assert(info.Doc, Matches, helpCommandsText)
- jc, _, err = initDefenestrate([]string{"discombobulate"})
+ jc, _, err := initDefenestrate([]string{"discombobulate"})
c.Assert(err, ErrorMatches, "unrecognized command: jujutest discombobulate")
info = jc.Info()
c.Assert(info.Name, Equals, "jujutest")
c.Assert(info.Args, Equals, "<command> ...")
- c.Assert(info.Doc, Equals, "commands:\n defenestrate - defenestrate the juju")
+ c.Assert(info.Doc, Matches, "commands:\n defenestrate - defenestrate the juju"+helpText)
jc, tc, err := initDefenestrate([]string{"defenestrate"})
c.Assert(err, IsNil)
@@ -51,7 +52,7 @@
}
func (s *SuperCommandSuite) TestRegister(c *C) {
- jc := &cmd.SuperCommand{Name: "jujutest"}
+ jc := cmd.NewSuperCommand(cmd.SuperCommandParams{Name: "jujutest"})
jc.Register(&TestCommand{Name: "flip"})
jc.Register(&TestCommand{Name: "flap"})
badCall := func() { jc.Register(&TestCommand{Name: "flap"}) }
@@ -59,14 +60,15 @@
}
func (s *SuperCommandSuite) TestRegisterAlias(c *C) {
- jc := &cmd.SuperCommand{Name: "jujutest"}
+ jc := cmd.NewSuperCommand(cmd.SuperCommandParams{Name: "jujutest"})
jc.Register(&TestCommand{Name: "flip", Aliases: []string{"flap", "flop"}})
info := jc.Info()
c.Assert(info.Doc, Equals, `commands:
flap - alias for flip
flip - flip the juju
- flop - alias for flip`)
+ flop - alias for flip
+ help - show help on a command or other topic`)
}
var commandsDoc = `commands:
@@ -74,35 +76,40 @@
flip - flip the juju`
func (s *SuperCommandSuite) TestInfo(c *C) {
- jc := &cmd.SuperCommand{
- Name: "jujutest", Purpose: "to be purposeful", Doc: "doc\nblah\ndoc",
- }
+ jc := cmd.NewSuperCommand(cmd.SuperCommandParams{
+ Name: "jujutest",
+ Purpose: "to be purposeful",
+ Doc: "doc\nblah\ndoc",
+ })
info := jc.Info()
c.Assert(info.Name, Equals, "jujutest")
c.Assert(info.Purpose, Equals, "to be purposeful")
- c.Assert(info.Doc, Equals, jc.Doc)
+ // info doc starts with the jc.Doc and ends with the help command
+ c.Assert(info.Doc, Matches, jc.Doc+"(.|\n)*")
+ c.Assert(info.Doc, Matches, "(.|\n)*"+helpCommandsText)
jc.Register(&TestCommand{Name: "flip"})
jc.Register(&TestCommand{Name: "flapbabble"})
info = jc.Info()
- c.Assert(info.Doc, Equals, jc.Doc+"\n\n"+commandsDoc)
+ c.Assert(info.Doc, Matches, jc.Doc+"\n\n"+commandsDoc+helpText)
jc.Doc = ""
info = jc.Info()
- c.Assert(info.Doc, Equals, commandsDoc)
+ c.Assert(info.Doc, Matches, commandsDoc+helpText)
}
func (s *SuperCommandSuite) TestLogging(c *C) {
- target, debug := log.Target, log.Debug
+ target, debug := log.Target(), log.Debug
defer func() {
- log.Target, log.Debug = target, debug
+ log.SetTarget(target)
+ log.Debug = debug
}()
- jc := &cmd.SuperCommand{Name: "jujutest", Log: &cmd.Log{}}
+ jc := cmd.NewSuperCommand(cmd.SuperCommandParams{Name: "jujutest", Log: &cmd.Log{}})
jc.Register(&TestCommand{Name: "blah"})
ctx := testing.Context(c)
code := cmd.Main(jc, ctx, []string{"blah", "--option", "error", "--debug"})
c.Assert(code, Equals, 1)
- c.Assert(bufferString(ctx.Stderr), Matches, `.* JUJU jujutest blah command failed: BAM!
+ c.Assert(bufferString(ctx.Stderr), Matches, `^.* ERROR JUJU:jujutest:blah jujutest blah command failed: BAM!
error: BAM!
`)
}
=== modified file 'cmd/util_test.go'
--- cmd/util_test.go 2013-02-28 12:27:09 +0000
+++ cmd/util_test.go 2013-04-08 08:09:22 +0000
@@ -15,6 +15,7 @@
// TestCommand is used by several different tests.
type TestCommand struct {
+ cmd.CommandBase
Name string
Option string
Minimal bool
=== renamed file 'cmd/jujud/version.go' => 'cmd/version.go'
--- cmd/jujud/version.go 2013-02-20 22:11:47 +0000
+++ cmd/version.go 2013-04-08 08:09:22 +0000
@@ -1,31 +1,27 @@
-package main
+package cmd
import (
"launchpad.net/gnuflag"
- "launchpad.net/juju-core/cmd"
"launchpad.net/juju-core/version"
)
// VersionCommand is a cmd.Command that prints the current version.
type VersionCommand struct {
- out cmd.Output
+ CommandBase
+ out Output
}
-func (v *VersionCommand) Info() *cmd.Info {
- return &cmd.Info{
+func (v *VersionCommand) Info() *Info {
+ return &Info{
Name: "version",
Purpose: "print the current version",
}
}
func (v *VersionCommand) SetFlags(f *gnuflag.FlagSet) {
- v.out.AddFlags(f, "smart", cmd.DefaultFormatters)
-}
-
-func (v *VersionCommand) Init(args []string) error {
- return cmd.CheckEmpty(args)
-}
-
-func (v *VersionCommand) Run(ctxt *cmd.Context) error {
+ v.out.AddFlags(f, "smart", DefaultFormatters)
+}
+
+func (v *VersionCommand) Run(ctxt *Context) error {
return v.out.Write(ctxt, version.Current.String())
}
=== renamed file 'cmd/jujud/version_test.go' => 'cmd/version_test.go'
--- cmd/jujud/version_test.go 2012-08-16 17:09:22 +0000
+++ cmd/version_test.go 2013-04-08 08:09:22 +0000
@@ -1,10 +1,9 @@
-package main
+package cmd
import (
"bytes"
"fmt"
. "launchpad.net/gocheck"
- "launchpad.net/juju-core/cmd"
"launchpad.net/juju-core/version"
)
@@ -14,11 +13,11 @@
func (s *VersionSuite) TestVersion(c *C) {
var stdout, stderr bytes.Buffer
- ctx := &cmd.Context{
+ ctx := &Context{
Stdout: &stdout,
Stderr: &stderr,
}
- code := cmd.Main(&VersionCommand{}, ctx, nil)
+ code := Main(&VersionCommand{}, ctx, nil)
c.Check(code, Equals, 0)
c.Assert(stderr.String(), Equals, "")
c.Assert(stdout.String(), Equals, version.Current.String()+"\n")
@@ -26,11 +25,11 @@
func (s *VersionSuite) TestVersionExtraArgs(c *C) {
var stdout, stderr bytes.Buffer
- ctx := &cmd.Context{
+ ctx := &Context{
Stdout: &stdout,
Stderr: &stderr,
}
- code := cmd.Main(&VersionCommand{}, ctx, []string{"foo"})
+ code := Main(&VersionCommand{}, ctx, []string{"foo"})
c.Check(code, Equals, 2)
c.Assert(stdout.String(), Equals, "")
c.Assert(stderr.String(), Matches, "error: unrecognized args.*\n")
@@ -38,11 +37,11 @@
func (s *VersionSuite) TestVersionJson(c *C) {
var stdout, stderr bytes.Buffer
- ctx := &cmd.Context{
+ ctx := &Context{
Stdout: &stdout,
Stderr: &stderr,
}
- code := cmd.Main(&VersionCommand{}, ctx, []string{"--format", "json"})
+ code := Main(&VersionCommand{}, ctx, []string{"--format", "json"})
c.Check(code, Equals, 0)
c.Assert(stderr.String(), Equals, "")
c.Assert(stdout.String(), Equals, fmt.Sprintf("%q", version.Current.String())+"\n")
=== added directory 'constraints'
=== renamed file 'state/constraints.go' => 'constraints/constraints.go'
--- state/constraints.go 2013-02-24 20:59:57 +0000
+++ constraints/constraints.go 2013-04-08 08:09:22 +0000
@@ -1,19 +1,17 @@
-package state
+package constraints
import (
"fmt"
- "labix.org/v2/mgo"
- "labix.org/v2/mgo/txn"
"math"
"strconv"
"strings"
)
-// Constraints describes a user's requirements of the hardware on which units
+// Value describes a user's requirements of the hardware on which units
// of a service will run. Constraints are used to choose an existing machine
// onto which a unit will be deployed, or to provision a new machine if no
// existing one satisfies the requirements.
-type Constraints struct {
+type Value struct {
// Arch, if not nil or empty, indicates that a machine must run the named
// architecture.
@@ -33,20 +31,20 @@
Mem *uint64 `json:"mem,omitempty" yaml:"mem,omitempty"`
}
-// String expresses a Constraints in the language in which it was specified.
-func (c Constraints) String() string {
+// String expresses a constraints.Value in the language in which it was specified.
+func (v Value) String() string {
var strs []string
- if c.Arch != nil {
- strs = append(strs, "arch="+*c.Arch)
- }
- if c.CpuCores != nil {
- strs = append(strs, "cpu-cores="+uintStr(*c.CpuCores))
- }
- if c.CpuPower != nil {
- strs = append(strs, "cpu-power="+uintStr(*c.CpuPower))
- }
- if c.Mem != nil {
- s := uintStr(*c.Mem)
+ if v.Arch != nil {
+ strs = append(strs, "arch="+*v.Arch)
+ }
+ if v.CpuCores != nil {
+ strs = append(strs, "cpu-cores="+uintStr(*v.CpuCores))
+ }
+ if v.CpuPower != nil {
+ strs = append(strs, "cpu-power="+uintStr(*v.CpuPower))
+ }
+ if v.Mem != nil {
+ s := uintStr(*v.Mem)
if s != "" {
s += "M"
}
@@ -55,6 +53,24 @@
return strings.Join(strs, " ")
}
+// WithFallbacks returns a copy of v with nil values taken from v0.
+func (v Value) WithFallbacks(v0 Value) Value {
+ v1 := v0
+ if v.Arch != nil {
+ v1.Arch = v.Arch
+ }
+ if v.CpuCores != nil {
+ v1.CpuCores = v.CpuCores
+ }
+ if v.CpuPower != nil {
+ v1.CpuPower = v.CpuPower
+ }
+ if v.Mem != nil {
+ v1.Mem = v.Mem
+ }
+ return v1
+}
+
func uintStr(i uint64) string {
if i == 0 {
return ""
@@ -62,11 +78,11 @@
return fmt.Sprintf("%d", i)
}
-// ParseConstraints constructs a Constraints from the supplied arguments,
+// Parse constructs a constraints.Value from the supplied arguments,
// each of which must contain only spaces and name=value pairs. If any
// name is specified more than once, an error is returned.
-func ParseConstraints(args ...string) (Constraints, error) {
- cons := Constraints{}
+func Parse(args ...string) (Value, error) {
+ cons := Value{}
for _, arg := range args {
raws := strings.Split(strings.TrimSpace(arg), " ")
for _, raw := range raws {
@@ -74,15 +90,43 @@
continue
}
if err := cons.setRaw(raw); err != nil {
- return Constraints{}, err
+ return Value{}, err
}
}
}
return cons, nil
}
+// MustParse constructs a constraints.Value from the supplied arguments,
+// as Parse, but panics on failure.
+func MustParse(args ...string) Value {
+ v, err := Parse(args...)
+ if err != nil {
+ panic(err)
+ }
+ return v
+}
+
+// Constraints implements gnuflag.Value for a Constraints.
+type ConstraintsValue struct {
+ Target *Value
+}
+
+func (v ConstraintsValue) Set(s string) error {
+ cons, err := Parse(s)
+ if err != nil {
+ return err
+ }
+ *v.Target = cons
+ return nil
+}
+
+func (v ConstraintsValue) String() string {
+ return v.Target.String()
+}
+
// setRaw interprets a name=value string and sets the supplied value.
-func (c *Constraints) setRaw(raw string) error {
+func (v *Value) setRaw(raw string) error {
eq := strings.Index(raw, "=")
if eq <= 0 {
return fmt.Errorf("malformed constraint %q", raw)
@@ -91,13 +135,13 @@
var err error
switch name {
case "arch":
- err = c.setArch(str)
+ err = v.setArch(str)
case "cpu-cores":
- err = c.setCpuCores(str)
+ err = v.setCpuCores(str)
case "cpu-power":
- err = c.setCpuPower(str)
+ err = v.setCpuPower(str)
case "mem":
- err = c.setMem(str)
+ err = v.setMem(str)
default:
return fmt.Errorf("unknown constraint %q", name)
}
@@ -107,8 +151,8 @@
return nil
}
-func (c *Constraints) setArch(str string) error {
- if c.Arch != nil {
+func (v *Value) setArch(str string) error {
+ if v.Arch != nil {
return fmt.Errorf("already set")
}
switch str {
@@ -117,28 +161,28 @@
default:
return fmt.Errorf("%q not recognized", str)
}
- c.Arch = &str
+ v.Arch = &str
return nil
}
-func (c *Constraints) setCpuCores(str string) (err error) {
- if c.CpuCores != nil {
- return fmt.Errorf("already set")
- }
- c.CpuCores, err = parseUint64(str)
- return
-}
-
-func (c *Constraints) setCpuPower(str string) (err error) {
- if c.CpuPower != nil {
- return fmt.Errorf("already set")
- }
- c.CpuPower, err = parseUint64(str)
- return
-}
-
-func (c *Constraints) setMem(str string) error {
- if c.Mem != nil {
+func (v *Value) setCpuCores(str string) (err error) {
+ if v.CpuCores != nil {
+ return fmt.Errorf("already set")
+ }
+ v.CpuCores, err = parseUint64(str)
+ return
+}
+
+func (v *Value) setCpuPower(str string) (err error) {
+ if v.CpuPower != nil {
+ return fmt.Errorf("already set")
+ }
+ v.CpuPower, err = parseUint64(str)
+ return
+}
+
+func (v *Value) setMem(str string) error {
+ if v.Mem != nil {
return fmt.Errorf("already set")
}
var value uint64
@@ -155,7 +199,7 @@
val *= mult
value = uint64(math.Ceil(val))
}
- c.Mem = &value
+ v.Mem = &value
return nil
}
@@ -177,57 +221,3 @@
"T": 1024 * 1024,
"P": 1024 * 1024 * 1024,
}
-
-// constraintsDoc is the mongodb representation of a Constraints.
-type constraintsDoc struct {
- Arch *string
- CpuCores *uint64
- CpuPower *uint64
- Mem *uint64
-}
-
-func newConstraintsDoc(cons Constraints) constraintsDoc {
- return constraintsDoc{
- Arch: cons.Arch,
- CpuCores: cons.CpuCores,
- CpuPower: cons.CpuPower,
- Mem: cons.Mem,
- }
-}
-
-func createConstraintsOp(st *State, id string, cons Constraints) txn.Op {
- return txn.Op{
- C: st.constraints.Name,
- Id: id,
- Assert: txn.DocMissing,
- Insert: newConstraintsDoc(cons),
- }
-}
-
-func readConstraints(st *State, id string) (Constraints, error) {
- doc := constraintsDoc{}
- if err := st.constraints.FindId(id).One(&doc); err == mgo.ErrNotFound {
- return Constraints{}, NotFoundf("constraints")
- } else if err != nil {
- return Constraints{}, err
- }
- return Constraints{
- Arch: doc.Arch,
- CpuCores: doc.CpuCores,
- CpuPower: doc.CpuPower,
- Mem: doc.Mem,
- }, nil
-}
-
-func writeConstraints(st *State, id string, cons Constraints) error {
- ops := []txn.Op{{
- C: st.constraints.Name,
- Id: id,
- Assert: txn.DocExists,
- Update: D{{"$set", newConstraintsDoc(cons)}},
- }}
- if err := st.runner.Run(ops, "", nil); err != nil {
- return fmt.Errorf("cannot set constraints: %v", err)
- }
- return nil
-}
=== renamed file 'state/constraints_test.go' => 'constraints/constraints_test.go'
--- state/constraints_test.go 2013-02-25 12:57:18 +0000
+++ constraints/constraints_test.go 2013-04-08 08:09:22 +0000
@@ -1,12 +1,17 @@
-package state_test
+package constraints_test
import (
"encoding/json"
. "launchpad.net/gocheck"
"launchpad.net/goyaml"
- "launchpad.net/juju-core/state"
+ "launchpad.net/juju-core/constraints"
+ "testing"
)
+func TestPackage(t *testing.T) {
+ TestingT(t)
+}
+
type ConstraintsSuite struct{}
var _ = Suite(&ConstraintsSuite{})
@@ -184,14 +189,14 @@
func (s *ConstraintsSuite) TestParseConstraints(c *C) {
for i, t := range parseConstraintsTests {
c.Logf("test %d: %s", i, t.summary)
- cons0, err := state.ParseConstraints(t.args...)
+ cons0, err := constraints.Parse(t.args...)
if t.err == "" {
c.Assert(err, IsNil)
} else {
c.Assert(err, ErrorMatches, t.err)
continue
}
- cons1, err := state.ParseConstraints(cons0.String())
+ cons1, err := constraints.Parse(cons0.String())
c.Assert(err, IsNil)
c.Assert(cons1, DeepEquals, cons0)
}
@@ -205,7 +210,7 @@
return &s
}
-var constraintsRoundtripTests = []state.Constraints{
+var constraintsRoundtripTests = []constraints.Value{
{},
// {Arch: strp("")}, goyaml bug lp:1132537
{Arch: strp("amd64")},
@@ -223,10 +228,21 @@
},
}
+func (s *ConstraintsSuite) TestRoundtripGnuflagValue(c *C) {
+ for i, t := range constraintsRoundtripTests {
+ c.Logf("test %d", i)
+ var cons constraints.Value
+ val := constraints.ConstraintsValue{&cons}
+ err := val.Set(t.String())
+ c.Assert(err, IsNil)
+ c.Assert(cons, DeepEquals, t)
+ }
+}
+
func (s *ConstraintsSuite) TestRoundtripString(c *C) {
for i, t := range constraintsRoundtripTests {
c.Logf("test %d", i)
- cons, err := state.ParseConstraints(t.String())
+ cons, err := constraints.Parse(t.String())
c.Assert(err, IsNil)
c.Assert(cons, DeepEquals, t)
}
@@ -237,7 +253,7 @@
c.Logf("test %d", i)
data, err := json.Marshal(t)
c.Assert(err, IsNil)
- var cons state.Constraints
+ var cons constraints.Value
err = json.Unmarshal(data, &cons)
c.Assert(err, IsNil)
c.Assert(cons, DeepEquals, t)
@@ -250,7 +266,7 @@
data, err := goyaml.Marshal(t)
c.Assert(err, IsNil)
c.Logf("%s", data)
- var cons state.Constraints
+ var cons constraints.Value
err = goyaml.Unmarshal(data, &cons)
c.Assert(err, IsNil)
c.Assert(cons, DeepEquals, t)
@@ -266,3 +282,86 @@
// delete this test and uncomment the flagged constraintsRoundtripTests.
c.Assert(val.Hello, IsNil)
}
+
+var withFallbacksTests = []struct {
+ desc string
+ initial string
+ fallbacks string
+ final string
+}{
+ {
+ desc: "empty all round",
+ }, {
+ desc: "arch with empty fallback",
+ initial: "arch=amd64",
+ final: "arch=amd64",
+ }, {
+ desc: "arch with ignored fallback",
+ initial: "arch=amd64",
+ fallbacks: "arch=i386",
+ final: "arch=amd64",
+ }, {
+ desc: "arch from fallback",
+ fallbacks: "arch=i386",
+ final: "arch=i386",
+ }, {
+ desc: "cpu-cores with empty fallback",
+ initial: "cpu-cores=2",
+ final: "cpu-cores=2",
+ }, {
+ desc: "cpu-cores with ignored fallback",
+ initial: "cpu-cores=4",
+ fallbacks: "cpu-cores=8",
+ final: "cpu-cores=4",
+ }, {
+ desc: "cpu-cores from fallback",
+ fallbacks: "cpu-cores=8",
+ final: "cpu-cores=8",
+ }, {
+ desc: "cpu-power with empty fallback",
+ initial: "cpu-power=100",
+ final: "cpu-power=100",
+ }, {
+ desc: "cpu-power with ignored fallback",
+ initial: "cpu-power=100",
+ fallbacks: "cpu-power=200",
+ final: "cpu-power=100",
+ }, {
+ desc: "cpu-power from fallback",
+ fallbacks: "cpu-power=200",
+ final: "cpu-power=200",
+ }, {
+ desc: "mem with empty fallback",
+ initial: "mem=4G",
+ final: "mem=4G",
+ }, {
+ desc: "mem with ignored fallback",
+ initial: "mem=4G",
+ fallbacks: "mem=8G",
+ final: "mem=4G",
+ }, {
+ desc: "mem from fallback",
+ fallbacks: "mem=8G",
+ final: "mem=8G",
+ }, {
+ desc: "non-overlapping mix",
+ initial: "mem=4G arch=amd64",
+ fallbacks: "cpu-power=1000 cpu-cores=4",
+ final: "mem=4G arch=amd64 cpu-power=1000 cpu-cores=4",
+ }, {
+ desc: "overlapping mix",
+ initial: "mem=4G arch=amd64",
+ fallbacks: "cpu-power=1000 cpu-cores=4 mem=8G",
+ final: "mem=4G arch=amd64 cpu-power=1000 cpu-cores=4",
+ },
+}
+
+func (s *ConstraintsSuite) TestWithFallbacks(c *C) {
+ for i, t := range withFallbacksTests {
+ c.Logf("test %d", i)
+ initial := constraints.MustParse(t.initial)
+ fallbacks := constraints.MustParse(t.fallbacks)
+ final := constraints.MustParse(t.final)
+ c.Assert(initial.WithFallbacks(fallbacks), DeepEquals, final)
+ }
+}
=== added file 'doc/api.txt'
--- doc/api.txt 1970-01-01 00:00:00 +0000
+++ doc/api.txt 2013-04-08 08:09:22 +0000
@@ -0,0 +1,319 @@
+* API Design
+
+The overall aim is to make agents and clients connect through a network
+API rather than directly to the underlying database as is the case
+currently.
+
+This will have a few advantages:
+
+- Operations that involve multiple round trips to the database can be
+made more efficient because the API server is likely to be closer to
+the database server.
+
+- We can decide on, and enforce, an appropriate authorization policy
+for each operation.
+
+- The API can be made easy to use from multiple languages and client
+types.
+
+There are two general kinds of operation on the API: simple
+requests and watch requests. I'll deal with simple requests
+first.
+
+* Simple requests
+
+A simple request takes some parameters, possibly makes some changes to
+the state database, and returns some results or an error.
+
+When the request returns no data, it would theoretically be possible to
+have the API server operate on the request without returning a reply,
+but then the client would not know when the request has completed or if
+it completed successfully. Therefore, I think it's better if all requests
+return a reply.
+
+Here is the list of all the State requests that are currently used by the
+juju agents:
+ XXX link
+We will need to implement at least these requests (possibly
+slightly changed, but hopefully as little as possible, to ensure as
+little churn as possible in the agent code when moving to using the API)
+
+41 out of the 59 requests are operating directly on a single state
+entity, expressed as the receiver object in the Go API. For this reason
+I believe it's appropriate to phrase the API requests in this way -
+as requests on particular entities in the state. This leads to certain
+implementation advantages (see "Implementation" below) and means there's
+a close correspondence between the API protocol and the API as implemented
+in Go (and hopefully other languages too).
+
+To make the protocol accessible, we define all messages to be in JSON
+format and we use a secure websocket for transport.
+For security, we currently rely on a server-side certificate
+and passwords sent over the connection to identify the client,
+but it should be straightforward to enable the server to
+do client certificate checking if desired.
+
+Here's a sample request to change the instance id
+associated with a machine, and its reply. (I'll show JSON in rjson form, to keep the
+noise down, see http://godoc.org/launchpad.net/rjson).
+
+Client->Server
+ {
+ RequestId: 1234
+ Type: "Machine"
+ Id: "99"
+ Request: "SetInstanceId"
+ Params: {
+ InstanceId: "i-43e55e5"
+ }
+ }
+Server->Client
+ {
+ RequestId: 1234
+ Error: ""
+ Result: {
+ }
+ }
+
+We use the RequestId field to associate the request and its
+reply. The API client must not re-use a request id until
+it has received the request's reply (the easiest way
+to do that is simply to increment the request id each time).
+We allow multiple requests to be outstanding on a connection
+at once, and their replies can be received in any order.
+
+In the request, the Id field may be omitted to specify
+an empty Id, and Params may be omitted
+to specify no request parameters. Similarly, in the
+response, the Error field may be omitted to
+signify no error, and the Result field may be
+omitted to signify no result. To save space below,
+I've omitted fields accordingly.
+
+The Type field identifies the type of entity to act on,
+and the Id field its identifier. Currently I envisage
+the following types of entities:
+
+ Admin
+ Admin (a singleton) is used by a client when identifying itself
+ to the server. It is the only thing that can be accessed
+ before the client has authenticated.
+
+ Client
+ ClientWatcher
+ Client (a singleton) is the access point for all the GUI client
+ and other user-facing methods. This is only
+ usable by clients, not by agents.
+
+ State
+ Machine
+ Unit
+ Relation
+ RelationUnit
+ Service
+ Pinger
+ MachineWatcher
+ UnitWatcher
+ LifecycleWatcher
+ ServiceUnitsWatcher
+ ServiceRelationsWatcher
+ RelationScopeWatcher
+ UnitsWatcher
+ ConfigWatcher
+ EntityWatcher
+ MachineUnitsWatcher
+ These correspond directly to types exported by the
+ juju state package. They are usable only by agents,
+ not clients.
+
+The Request field specifies the action to perform, and Params holds the
+parameters to that request.
+
+In the reply message, the RequestId field must match that of the
+request. If the request failed, then the Error field holds the description
+of the error (it is possible we might add a Code field later, to help
+diagnosing specific kinds of error).
+
+The Result field holds the results of the request (in this case there
+are none, so it's empty).
+
+That completes the overview of simple requests,
+so on to watching.
+
+* Watching
+
+To watch something in the state, we invoke a Watch request, which
+returns a handle to a watcher object, that can then be used to find
+out when changes happen by calling its Next method. To stop a watcher,
+we call Stop on it.
+
+For example, if an agent wishes to watch machine 99, the conversation
+with the API server looks something like this:
+
+Client->Server
+ {
+ RequestId: 1000
+ Type: "Machine"
+ Id: "99"
+ Request: "Watch"
+ }
+Server->Client
+ {
+ RequestId: 1000
+ Response: {
+ EntityWatcherId: "1"
+ }
+ }
+
+At this point, the watcher is registered. Subsequent Next calls will
+only return when the entity has changed.
+
+Client->Server
+ {
+ RequestId: 1001
+ Type: "EntityWatcher"
+ Id: "1"
+ Request: "Next"
+ }
+
+This reply will only sent when something has changed. Note that for this
+particular watcher, no data is sent with the Next response. This can vary
+according to the particular kind of watcher - some watchers may return
+deltas, for example, or the latest value of the thing being watched.
+
+Server->Client
+ {
+ RequestId: 1001
+ }
+
+The client can carry on sending Next requests for
+as long as it chooses, each one returning only
+when the machine has changed since the previous
+Next request.
+
+Client->Server
+ {
+ RequestId: 1002
+ Type: "EntityWatcher"
+ Id: "1"
+ Request: "Next"
+ }
+
+Finally, the client decides to stop the watcher. This
+causes any outstanding Next request to return too -
+in no particular order with respect to the Stop reply.
+
+Client->Server
+ {
+ RequestId: 1003
+ Type: "EntityWatcher"
+ Id: "1"
+ Request: "Stop"
+ }
+Server->Client
+ {
+ RequestId: 1002
+ }
+Server->Client
+ {
+ RequestId: 1003
+ }
+
+As you can see, we use exactly the same RPC mechanism for watching as
+for simple requests. An alternative would have been to push watch change
+notifications to clients without waiting for an explicit request.
+
+Both schemes have advantages and disadvantages. I've gone with the
+above scheme mainly because it makes the protocol more obviously correct
+in the face of clients that are not reading data fast enough - in the
+face of a client with a slow network connection, we will not continue
+saturating its link with changes that cannot be passed through the
+pipe fast enough. Because Juju is state-based rather than event-based,
+the number of possible changes is bounded by the size of the system,
+so even if a client is very slow at reading the number of changes pushed
+down to it will not grow without bound.
+
+Allocating a watcher per client also implies that the server must
+keep some per-client state, but preliminary measurements indicate
+that the cost of that is unlikely to be prohibitive.
+
+Using exactly the same mechanism for all interactions with the API has
+advantages in simplicity too.
+
+* Authentication and authorization
+
+The API server is authenticated by TLS handshake before the
+websocket connection is initiated; the client should check that
+the server's certificate is signed by a trusted CA (in particular
+the CA that's created as a part of the bootstrap process).
+
+One wrinkle here is that before bootstrapping, we don't
+know the DNS name of the API server (in general, from
+a high-availability standpoint, we want to be able to serve the API from
+any number of servers), so we cannot put it into
+the certificate that we generate for the API server.
+This doesn't sit well with the way that www authentication
+usually works - hopefully there's a way around it in node.
+
+The client authenticates to the server currently by providing
+a user name and password in a Login request:
+
+Client->Server
+ {
+ RequestId: 1
+ Type: "Admin"
+ Request: "Login"
+ Params: {
+ "Tag": "machine-1",
+ Password: "a2eaa54323ae",
+ }
+ }
+Server->Client
+ {
+ RequestId: 1
+ }
+
+Until the user has successfully logged in, the Login
+request is the only one that the server will respond
+to - all other requests yield a "permission denied"
+error.
+
+The exact form of the Login request is subject to change,
+depending on what kind user authentication we might
+end up with - it may even end up as two or more requests,
+going through different stages of some authentication
+process.
+
+When logged in, requests are authorized both at the
+type level (to filter out obviously inappropriate requests,
+such as a client trying to access the agent API) and
+at the request level (allowing a more fine-grained
+approach).
+
+* Versioning
+
+I'm not currently sure of the best approach to versioning.
+One possibility is to have a Version request that
+allows the client to specify a desired version number;
+the server could then reply with a lower (or
+equal) version. The server would then serve the
+version of the protocol that it replied with.
+
+Unfortunately, this adds an extra round trip to the
+session setup. This could be mitigated by sending
+both the Version and the Login requests at the same
+time.
+
+* Implementation
+
+The Go stack consists of the following levels (high to low):
+
+ client interface ("launchpad.net/juju-core/state/api".State)
+ rpc package ("launchpad.net/juju-core/rpc".Client)
+ ----- (json transport over secure websockets, implemented by 3rd party code)
+ rpc package ("launchpad.net/juju-core/rpc".Server)
+ server implementation ("launchpad.net/juju-core/state/api".Server)
+ server backend ("launchpad.net/juju-core/state")
+ mongo data store
+
+
=== added file 'doc/bazaar-pipelines.txt'
--- doc/bazaar-pipelines.txt 1970-01-01 00:00:00 +0000
+++ doc/bazaar-pipelines.txt 2013-04-08 08:09:22 +0000
@@ -0,0 +1,86 @@
+Bazaar Pipelines
+================
+
+Pipelines are implemented using a bazaar plugin.
+
+ $ mkdir -p ~/.bazaar/plugins
+ $ bzr branch lp:bzr-pipeline ~/.bazaar/plugins/pipeline
+
+Basic info for pipelines can be found using `bzr help pipeline`.
+
+Pipelines require lightweight checkouts, but that is how `cobzr` and how the
+recommendations are specified in the `bazaar-usage.txt` document.
+
+
+Why use pipelines
+=================
+
+Pipelines could be thought of as a doubly linked list of dependent branches.
+
+Often when working you need to break up the implementation, either because the
+work can be more easily reviewed as a collection of small independent changes,
+or the work can be landed incrementally.
+
+Another reason is to avoid mixing new work with other refactoring that occurs
+during the process of writing the new work. Often when adding new features,
+other parts of code need to change. The branch is easier to review if the
+prerequisite changes happen seperately.
+
+Sometimes you don't know you want to refactor things until half of it is done
+already. In this situation you can create a new pipe before the current one,
+and move the changes into it.
+
+ $ bzr add-pipe --before some-refactoring-work
+ $ bzr merge -i :next
+
+This enters you into an interactive merge of the changes from the next branch
+in the pipeline.
+
+
+Merging trunk
+=============
+
+When merging trunk into a pipeline, you should move to the first branch in the
+pipeline.
+
+ $ bzr switch :first
+ $ bzr merge <trunk>
+ # resolve any conflicts that may be there
+ $ bzr commit -m "Merge trunk"
+ $ bzr pump
+
+The pump command is effectively merging each pipe into the next pipe and
+commits without changing the current active pipe. The pump command starts
+with the active pipe. If there are conflicts from any particular merge, the
+pumping stops, and the active branch is set to be the branch that had the
+conflicts ready for you to fix the conflicts.
+
+
+Useful aliases
+==============
+
+ $ bzr alias pipes="show-pipeline"
+
+Show the branches in the pipeline. All branches are considered a pipeline
+with one branch, so you can run this on any branch (actually a lightweight
+checkout). The current pipe is shown with an `*` at the start of the line.
+
+ $ bzr alias next="switch-pipe :next"
+ $ bzr alias prev="switch-pipe :prev"
+
+These two aliases allow you to move around the pipeline using:
+
+ $ bzr next # move to the next branch in the pipeline
+ $ bzr prev # move to the previous branch in the pipeline
+
+
+ $ bzr alias pdiff="diff -r branch::prev"
+
+Show me the differences that this branch has introduced compared to the
+previous branch in the pipeline.
+
+ $ bzr alias unpumped="missing --mine :next"
+
+Show the revisions that are in the current branch that are not yet in the next
+branch in the pipeline.
+
=== added file 'doc/bazaar-usage.txt'
--- doc/bazaar-usage.txt 1970-01-01 00:00:00 +0000
+++ doc/bazaar-usage.txt 2013-04-08 08:09:22 +0000
@@ -0,0 +1,129 @@
+Bazaar Basics
+=============
+
+An alternative to using `cobzr` is to use the normal `bzr` with light-weight
+checkouts (see `bzr help checkouts`).
+
+The first step is to create a repository that contains the juju-core trunk and
+other working branches.
+
+
+The Repository
+==============
+
+See `bzr help repositories` for more info on repositories.
+
+For this example, we'll use ~/src as a location for the repository.
+
+ $ cd ~/src
+ $ bzr init-repo juju-core
+
+This will create a repository that has working trees (the actual files and
+directories - see `bzr help working-trees`.
+
+Now put trunk in there:
+
+ $ cd juju-core
+ $ bzr branch lp:juju-core trunk
+
+
+Working in $GOPATH
+==================
+
+Now that we have trunk of juju-core elsewhere, we now need to use it inside
+$GOPATH.
+
+These steps assume that you have juju-core already available in $GOPATH/src.
+
+ $ cd $GOPATH/src/launchpad.net/juju-core
+ $ bzr reconfigure --lightweight-checkout --bind-to ~/src/juju-core/trunk
+
+Now when you look at that branch, you should see the following
+
+ $ bzr info
+ Lightweight checkout (format: 2a)
+ Location:
+ light checkout root: .
+ checkout of branch: /home/<you>/src/juju-core/trunk
+ shared repository: /home/<you>/src/juju-core
+
+
+Making pushes easier
+====================
+
+You can specify information in the bazaar locations file which it uses to
+determine the locations of the public and push locations for a branch.
+
+Inside your ~/.bazaar/locations.conf file, add the following (not including
+the curly braces).
+
+{{{
+[/home/eric/src]
+public_branch = bzr+ssh://bazaar.launchpad.net/~eric-the-viking
+public_branch:policy = appendpath
+push_location = lp:~eric-the-viking
+push_location:policy = appendpath
+}}}
+
+And replace 'eric' with your login id, and 'eric-the-viking' with your
+launchpad id.
+
+The `appendpath` policy means that the directories under ~/src are added to
+the path, so ~/src/juju-core/trunk would be pushed to (by default)
+lp:~eric-the-viking/juju-core/trunk. What this means is that when you create
+a new branch `new-work`, and go `bzr push` it goes to
+`lp:~eric-the-viking/juju-core/new-work`.
+
+
+Making a branch to work in
+==========================
+
+Inside the $GOPATH/src/launchpad.net/juju-core directory, you can create a new
+branch to work on using:
+
+ $ bzr switch -b new-work
+
+This creates a new branch in `~/src/juju-core` called `new-work` and switches
+the working tree to use that. Commits are now on that new branch, and push
+sends it to launchpad to the `new-work` branch.
+
+Everything else works the same.
+
+
+Useful aliases
+==============
+
+ $ bzr alias commit="commit --strict"
+
+This will mean that whenever you use commit, it adds the `--strict` flag.
+What this means is that it will not allow you to commit if there are unknown
+files. This is very useful when you create new files but forget to add them
+prior to commit.
+
+If you do have unknown files and want to override the strict behaviour for one
+commit, then you can go...
+
+ $ bzr commit --no-strict -m "Blah blah"
+
+
+Another useful alias is:
+
+ $ bzr alias ll="log --line -r-10..-1"
+
+Will give you something like the following:
+
+{{{
+$ bzr ll
+956: Tim Penhey 2013-03-06 Add some documentation around lightweight checkout usage.
+955: Dave Cheney 2013-03-05 [merge] environs/ec2: try to get tests working on raring
+954: Roger Peppe 2013-03-04 [merge] juju: add NewConnFromState
+953: Dimiter Naydenov 2013-03-01 [merge] state, uniter: Units now use charm URLs
+952: Francesco Banconi 2013-03-01 [merge] Implement the API unexpose command.
+951: William Reade 2013-03-01 [merge] environs: drop InstanceIdAccessor hack
+950: Brad Crittenden 2013-02-28 [merge] Add the 'expose' command to the API.
+949: John A Meinel 2013-02-28 [merge] revert only r943
+948: William Reade 2013-02-28 [merge] history: rewind
+947: Ian Booth 2013-02-28 [merge] Better unauthorised errors
+}}}
+
+
=== renamed file 'doc/draft/charms-in-action.txt' => 'doc/charms-in-action.txt'
=== added file 'doc/commands.txt'
--- doc/commands.txt 1970-01-01 00:00:00 +0000
+++ doc/commands.txt 2013-04-08 08:09:22 +0000
@@ -0,0 +1,55 @@
+Commands and Sub-commands
+=========================
+
+The base `Command` interface is found in `cmd/cmd.go`.
+
+Commands need to provide an `Info` method that returns an Info struct.
+
+The info struct contains: name, args, purpose and a detailed description.
+This information is used to provide the default help for the command.
+
+In the same package, there is `CommandBase` whose purpose is to be composed
+into new commands, and provides a default no-op SetFlags implementation, a
+default Init method that checks for no extra args, and a default Help method.
+
+
+Supercommands
+=============
+
+`Supercommand`s are commands that do many things, and have "sub-commands" that
+provide this functionality. Git and Bazaar are common examples of
+"supercommands". Subcommands must also provide the `Command` interface, and
+are registered using the `Register` method. The name and aliases are
+registered with the supercommand. If there is a duplicate name registered,
+the whole thing panics.
+
+Supercommands need to be created with the `NewSuperCommand` function in order
+to provide a fully constructed object.
+
+The 'help' subcommand
+---------------------
+
+All supercommand instances get a help command. This provides the basic help
+functionality to get all the registered commands, with the addition of also
+being able to provide non-command help topics which can be added.
+
+Help topics have a `name` which is what is matched from the command line, a
+`short` one line description that is shown when `<cmd> help topics` is called,
+and a `long` text that is output when the topic is requested.
+
+Topics are added using the `AddHelpTopic` method.
+
+
+Execution
+=========
+
+The `Main` method in the cmd package handles the execution of a command.
+
+A new `gnuflag.FlagSet` is created and passed to the command in `SetFlags`.
+This is for the command to register the flags that it knows how to handle.
+
+The args are then parsed, and passed through to the `Init` method for the
+command to decide what to do with the positional arguments.
+
+The command is then `Run` and passed in an execution `Context` that defines
+the standard input and output streams, and has the current working directory.
=== renamed file 'doc/draft/death-and-destruction.txt' => 'doc/death-and-destruction.txt'
=== removed directory 'doc/draft'
=== renamed file 'doc/draft/entity-creation.txt' => 'doc/entity-creation.txt'
=== renamed file 'doc/draft/glossary.txt' => 'doc/glossary.txt'
=== added file 'doc/hacking-state.txt'
--- doc/hacking-state.txt 1970-01-01 00:00:00 +0000
+++ doc/hacking-state.txt 2013-04-08 08:09:22 +0000
@@ -0,0 +1,153 @@
+Hacking the juju-core/state package
+===================================
+
+This document remains a work in progress; it's an attempt to capture
+the various conventions and things to bear in mind that aren't
+necessarily written down anywhere else.
+
+return values: ok vs err
+------------------------
+
+By convention, anything that could reasonably fail must use a separate
+channel to communicate the failure. Broadly speaking, methods that can
+fail in more than one way must return an error; those that can only
+fail in one way (eg Machine.InstanceId: the value is either valid or
+missing) are expected to return a bool for consistency's sake, even if
+the type of the return value is such that failure can be signalled in-
+band.
+
+changes to entities
+-------------------
+
+Entity objects reflect remote state that may change at any time, but we
+don't want to make that too obvious. By convention, the only methods
+that should change an entity's in-memory document are as follows:
+
+ * Refresh(), which should update every field.
+ * Methods that set fields remotely should update only those fields
+ locally.
+
+The upshot of this is that it's not appropriate to call Refresh on any
+argument to a state method, including receivers; if you're in a context
+in which that would be helpful, you should clone the entity first. This
+is simple enough that there's no Clone() method, but it would be kinda
+nice to implement them in our Copious Free Time; I think there are
+places outside state that would also find it useful.
+
+care and feeding of mgo/txn
+---------------------------
+
+Just about all our writes to mongodb are mediated by the mgo/txn
+package, and using this correctly demands some care. Not all the code
+has been written in a fully aware state, and cases in which existing
+practice is divergent from the advice given below should be regarded
+with some suspicion.
+
+The txn package lets you make watchable changes to mongodb via lists of
+operations with the following critical properties:
+
+ * transactions can apply to more than one document (this is rather
+ the point of having them).
+ * transactions will complete if every assert in the transaction
+ passes; they will not run at all if any assert fails.
+ * multi-document transactions are *not* atomic; the operations are
+ applied in the order specified by the list.
+ * operations, and hence assertions, can only be applied to documents
+ with ids that are known at the time the operation list is built;
+ this means that it takes extra work to specify a condition like
+ "no unit document newer than X exists".
+
+The second point above deserves further discussion. Whenever you are
+implementing a state change, you should consider the impact of the
+following possible mongodb states on your code:
+
+ * if mongodb is already in a state consistent with the transaction
+ having already been run -- which is *always* possible -- you
+ should return immediately without error.
+ * if mongodb is in a state that indicates the transaction is not
+ valid -- eg trying to add a unit to a dying service -- you should
+ return immediately with a descriptive error.
+
+Each of the above situations should generally be checked as part of
+preparing a new []txn.Op, but in some cases it's convenient to trust
+(to begin with) that the entity's in-memory state reflects reality.
+Regardless, your job is to build a list of operations which assert
+that:
+
+ * the transaction still needs to be applied.
+ * the transaction is actively valid.
+ * facts on which the transaction list's form depends remain true.
+
+If you're really lucky you'll get to write a transaction in which
+the third requirement collapses to nothing, but that's not really
+the norm. In practice, you need to be prepared for the run to return
+txn.ErrAborted; if this happens, you need to check for previous success
+(and return nil) or for known cause of invalidity (and return an error
+describing it).
+
+If neither of these cases apply, you should assume it's an assertion
+failure of the third kind; in that case, you should build a new
+transaction based on more recent state and try again. If ErrAborteds
+just keep coming, give up; there's an ErrExcessiveContention that
+helps to describe the situation.
+
+watching entities, and select groups thereof
+--------------------------------------------
+
+The mgo/txn log enables very convenient notifications of changes to
+particular documents and groups thereof. The state/watcher package
+converts the txn event log into events and send them down client-
+supplied channels for further processing; the state code itself
+implements watchers in terms of these events.
+
+All the internally-relevant watching code is implemented in the file
+state/watcher.go. These constructs can be broadly divided into groups
+as follows:
+
+ * single-document watchers: dead simple, they notify of every
+ change to a given doc. SettingsWatcher bucks the convention of
+ EntityWatcher in that it reads whole *Settings~s to send down the
+ channel rather than just sending notifications (we probably
+ shouldn't do that, but I don't think there's time to fix it right
+ now).
+ * entity group watchers: pretty simple, very common; they notify of
+ every change to the Life field among a group of entities in the
+ same collection, with group membership determined in a wide variety
+ of ways.
+ * relation watchers: of a similar nature to entity group watchers,
+ but generating carefully-ordered events from observed changes to
+ several different collections, none of which have Life fields.
+
+Implementation of new watchers is not necessarily a simple task, unless
+it's a genuinely trivial refactoring (or, given lack of generics, copy-
+and-paste job). Unless you already know exactly what you're doing,
+please start a discussion about what you're trying to do before you
+embark on further work in this area.
+
+transactions and reference counts
+---------------------------------
+
+As described above, it's difficult to assert things like "no units of
+service X exist". It can be done, but not without additional work, and
+we're generally using reference counts to enable this sort of thing.
+
+In general, reference counts should not be stored within entities: such
+changes are part of the operation of juju and are not important enough
+to be reflected as a constant stream of events. We didn't figure this
+out until recently, though, so relationDoc has a UnitCount, and
+serviceDoc has both a UnitCount and a RelationCount. This doesn't
+matter for the relation -- nothing watches relation docs directly --
+but I fear it matters quite hard for service, because every added or
+removed unit or relation will be an unnecessary event sent to every
+unit agent.
+
+We'll deal with that when it's a problem rather than just a concern;
+but new reference counts should not be thoughtlessly added to entity
+documents, and opportunities to separate them from existing docs should
+be considered seriously.
+
+
+[TODO: write about globalKey and what it's good for... constraints, settings,
+statuses, etc; occasionaly profitable use of fast prefix lookup on indexed
+fields.]
+
=== renamed file 'doc/draft/lifecycles.txt' => 'doc/lifecycles.txt'
=== added file 'doc/provisioning.txt'
--- doc/provisioning.txt 1970-01-01 00:00:00 +0000
+++ doc/provisioning.txt 2013-04-08 08:09:22 +0000
@@ -0,0 +1,113 @@
+What We Run, and Why
+====================
+
+Expressed as compactly as possible, the Provisioner is responsible for making
+sure that non-Dead machine entities in state have agents running on live
+instances; and for making sure that Dead machines, and stray instances, are
+removed and cleaned up.
+
+However, the choice of exactly what we deploy involves some subtleties. At the
+Provisioner level, it's simple: the series and the constraints we pass to the
+Environ.StartInstance come from the machine entity. But how did they get there?
+
+Series
+------
+
+Individual charms are released for different possible target series; juju
+should guarantee that charms for series X are only ever run on series X [0].
+Every service, unit, and machine has a series that's set at creation time and
+subsequently immutable. Units take their series from their service, and can
+only be assigned to machines with matching series.
+
+Subordinate units cannot be assigned directly to machines; they are created
+by their principals, on the same machine, in response to the creation of
+subordinate relations. We therefore restrict subordinate relations such that
+they can only be created between services with matching series.
+
+Constraints
+-----------
+
+Constraints are stored for environments, services, units, and machines, but
+unit constraints are not currently exposed because they're not needed outside
+state, and are likely to just cause trouble and confusion if we expose them.
+
+From the point of a user, there are environment constraints and service
+constraints, and sensible manipulations of them lead to predictable unit
+deployment decisions. The mechanism is as follows:
+
+ * when a unit is added, the current environment and service constraints
+ are collapsed into a single value and stored for the unit. (To be clear:
+ at the moment the unit is created, the current service and environment
+ constraints will be combined such that every constraint not set on the
+ service is taken from the environment (or left unset, if not specified
+ at all).
+ * when a machine is being added in order to host a given unit, it copies
+ its constraints directly from the unit.
+ * when a machine is being added without a unit associated -- for example,
+ when adding additional state servers -- it copies its constraints directly
+ from the environment.
+
+In this way the following sequence of operations becomes predictable:
+
+ $ juju deploy --constraints mem=2G wordpress
+ $ juju set-constraints --service wordpress mem=3G
+ $ juju add-unit wordpress -n 2
+
+...in that exactly one machine will be provisioned with the first set of
+constraints, and exactly two of them will be provisioned using the second
+set. This is much friendlier to the users than delaying the unit constraint
+capture and potentially suffering subtle and annoying races.
+
+Subordinate units cannot have constraints, because their deployment is
+controlled by their principal units. There's only ever one machine to which
+that subordinate could (and must) be deployed, and to restrict that further
+by means of constraints will only confuse people.
+
+Machine Status and Provisioning Errors (current)
+------------------------------------------------
+
+In the light of time pressure, a unit assigned to a machine that has not been
+provisioned can be removed directly by calling `juju destroy-unit`. Any
+provisioning error can thus be "resolved" in an unsophisticated but moderately
+effective way:
+
+ $ juju destroy-unit borken/0
+
+...in that at least broken units don't clutter up the service and prevent its
+removal. However:
+
+ $ juju destroy-machine 1
+
+...does not yet cause an unprovisioned machine to be removed from state (whether
+directly, or indirectly via the provisioner; the best place to implement this
+functionality is not clear).
+
+Machine Status and Provisioning Errors (WIP)
+--------------------------------------------
+
+[TODO: figure this out; not yet implemented, somewhat speculative... in
+particular, use of "resolved" may be inappropriate. Consider adding a
+"retry" CLI tool...]
+
+When the provisioner fails to start a machine, it should ensure that (1) the
+machine has no instance id set and (2) the machine has an error status set
+that communicates the nature of the problem. This must be visible in the
+output of `juju status`; and we must supply suitable tools to the user so
+as to allow her to respond appropriately.
+
+If the user believes a machine's provisioning error to be transient, she can
+do a simple `juju resolved 14` which will set some state to make machine 14
+eligible for the provisioner's attention again.
+
+It may otherwise be that the unit ended up snapshotting a service/environ
+config pair that really isn't satsifiable. In that case, the user can try
+(say) `juju resolved 14 --constraints "mem=2G cpu-power=400"`, which allows
+her to completely replace the machine's constraints as well as marking the
+machine for reprovisioning attention.
+
+
+[0] This is a little harder than it sounds -- in particular, openstack
+ environments don't necessarily have image series information available,
+ and may rely on the user to set a "default-image-id" config key that
+ happens to have the "right" series.
+
=== modified file 'downloader/downloader.go'
--- downloader/downloader.go 2012-08-20 16:11:45 +0000
+++ downloader/downloader.go 2013-04-08 08:09:22 +0000
@@ -100,7 +100,7 @@
if f != nil {
f.Close()
if err := os.Remove(f.Name()); err != nil {
- log.Printf("downloader: cannot remove temp file %q: %v", f.Name(), err)
+ log.Warningf("downloader: cannot remove temp file %q: %v", f.Name(), err)
}
}
}
=== modified file 'environs/agent/agent.go'
--- environs/agent/agent.go 2013-02-28 12:27:09 +0000
+++ environs/agent/agent.go 2013-04-08 08:09:22 +0000
@@ -8,7 +8,7 @@
"launchpad.net/juju-core/state/api"
"launchpad.net/juju-core/trivial"
"os"
- "path/filepath"
+ "path"
"regexp"
)
@@ -48,9 +48,9 @@
// ReadConf reads configuration data for the given
// entity from the given data directory.
-func ReadConf(dataDir, entityName string) (*Conf, error) {
- dir := Dir(dataDir, entityName)
- data, err := ioutil.ReadFile(filepath.Join(dir, "agent.conf"))
+func ReadConf(dataDir, tag string) (*Conf, error) {
+ dir := Dir(dataDir, tag)
+ data, err := ioutil.ReadFile(path.Join(dir, "agent.conf"))
if err != nil {
return nil, err
}
@@ -63,10 +63,10 @@
return nil, err
}
if c.StateInfo != nil {
- c.StateInfo.EntityName = entityName
+ c.StateInfo.Tag = tag
}
if c.APIInfo != nil {
- c.APIInfo.EntityName = entityName
+ c.APIInfo.Tag = tag
}
return &c, nil
}
@@ -77,25 +77,25 @@
// File returns the path of the given file in the agent's directory.
func (c *Conf) File(name string) string {
- return filepath.Join(c.Dir(), name)
+ return path.Join(c.Dir(), name)
}
func (c *Conf) confFile() string {
return c.File("agent.conf")
}
-// EntityName returns the entity name that will be used to connect to
-// the state.
-func (c *Conf) EntityName() string {
+// Tag returns the tag of the entity on whose behalf the state connection will
+// be made.
+func (c *Conf) Tag() string {
if c.StateInfo != nil {
- return c.StateInfo.EntityName
+ return c.StateInfo.Tag
}
- return c.APIInfo.EntityName
+ return c.APIInfo.Tag
}
// Dir returns the agent's directory.
func (c *Conf) Dir() string {
- return Dir(c.DataDir, c.EntityName())
+ return Dir(c.DataDir, c.Tag())
}
// Check checks that the configuration has all the required elements.
@@ -107,8 +107,8 @@
return requiredError("state info or API info")
}
if c.StateInfo != nil {
- if c.StateInfo.EntityName == "" {
- return requiredError("state entity name")
+ if c.StateInfo.Tag == "" {
+ return requiredError("state entity tag")
}
if err := checkAddrs(c.StateInfo.Addrs, "state server address"); err != nil {
return err
@@ -119,8 +119,8 @@
}
// TODO(rog) make APIInfo mandatory
if c.APIInfo != nil {
- if c.APIInfo.EntityName == "" {
- return requiredError("API entity name")
+ if c.APIInfo.Tag == "" {
+ return requiredError("API entity tag")
}
if err := checkAddrs(c.APIInfo.Addrs, "API server address"); err != nil {
return err
@@ -129,8 +129,8 @@
return requiredError("API CA certficate")
}
}
- if c.StateInfo != nil && c.APIInfo != nil && c.StateInfo.EntityName != c.APIInfo.EntityName {
- return fmt.Errorf("mismatched entity names")
+ if c.StateInfo != nil && c.APIInfo != nil && c.StateInfo.Tag != c.APIInfo.Tag {
+ return fmt.Errorf("mismatched entity tags")
}
return nil
}
@@ -200,8 +200,9 @@
// set the entity's password accordingly.
func (c *Conf) OpenState() (st *state.State, newPassword string, err error) {
info := *c.StateInfo
+ opts := state.DefaultDialOpts()
if info.Password != "" {
- st, err := state.Open(&info)
+ st, err := state.Open(&info, opts)
if err == nil {
return st, "", nil
}
@@ -214,7 +215,7 @@
// with the old password.
}
info.Password = c.OldPassword
- st, err = state.Open(&info)
+ st, err = state.Open(&info, opts)
if err != nil {
return nil, "", err
}
=== modified file 'environs/agent/agent_test.go'
--- environs/agent/agent_test.go 2013-01-24 12:36:09 +0000
+++ environs/agent/agent_test.go 2013-04-08 08:09:22 +0000
@@ -31,10 +31,10 @@
conf: agent.Conf{
OldPassword: "old password",
StateInfo: &state.Info{
- Addrs: []string{"foo.com:355", "bar:545"},
- CACert: []byte("ca cert"),
- EntityName: "entity",
- Password: "current password",
+ Addrs: []string{"foo.com:355", "bar:545"},
+ CACert: []byte("ca cert"),
+ Tag: "entity",
+ Password: "current password",
},
},
}, {
@@ -46,16 +46,16 @@
APIPort: 4321,
OldPassword: "old password",
StateInfo: &state.Info{
- Addrs: []string{"foo.com:355", "bar:545"},
- CACert: []byte("ca cert"),
- EntityName: "entity",
- Password: "current password",
+ Addrs: []string{"foo.com:355", "bar:545"},
+ CACert: []byte("ca cert"),
+ Tag: "entity",
+ Password: "current password",
},
APIInfo: &api.Info{
- EntityName: "entity",
- Password: "other password",
- Addrs: []string{"foo.com:555", "bar:555"},
- CACert: []byte("api ca cert"),
+ Tag: "entity",
+ Password: "other password",
+ Addrs: []string{"foo.com:555", "bar:555"},
+ CACert: []byte("api ca cert"),
},
},
}, {
@@ -63,56 +63,56 @@
conf: agent.Conf{
OldPassword: "old password",
StateInfo: &state.Info{
- Addrs: []string{"foo.com:355", "bar:545"},
- CACert: []byte("ca cert"),
- EntityName: "entity",
- Password: "current password",
- },
- APIInfo: &api.Info{
- EntityName: "entity",
- Addrs: []string{"foo.com:555"},
- CACert: []byte("ca cert"),
- },
- },
-}, {
- about: "no api entity name",
- conf: agent.Conf{
- StateServerCert: []byte("server cert"),
- StateServerKey: []byte("server key"),
- OldPassword: "old password",
- StateInfo: &state.Info{
- Addrs: []string{"foo.com:355", "bar:545"},
- CACert: []byte("ca cert"),
- EntityName: "entity",
- Password: "current password",
- },
- APIInfo: &api.Info{
- Addrs: []string{"foo.com:555"},
- CACert: []byte("api ca cert"),
- },
- },
- checkErr: "API entity name not found in configuration",
-}, {
- about: "mismatched entity names",
- conf: agent.Conf{
- StateServerCert: []byte("server cert"),
- StateServerKey: []byte("server key"),
- OldPassword: "old password",
- StateInfo: &state.Info{
- Addrs: []string{"foo.com:355", "bar:545"},
- CACert: []byte("ca cert"),
- EntityName: "entity",
- Password: "current password",
- },
- APIInfo: &api.Info{
- EntityName: "other",
- Addrs: []string{"foo.com:555"},
- CACert: []byte("api ca cert"),
- },
- },
- checkErr: "mismatched entity names",
-}, {
- about: "no state entity name",
+ Addrs: []string{"foo.com:355", "bar:545"},
+ CACert: []byte("ca cert"),
+ Tag: "entity",
+ Password: "current password",
+ },
+ APIInfo: &api.Info{
+ Tag: "entity",
+ Addrs: []string{"foo.com:555"},
+ CACert: []byte("ca cert"),
+ },
+ },
+}, {
+ about: "no api entity tag",
+ conf: agent.Conf{
+ StateServerCert: []byte("server cert"),
+ StateServerKey: []byte("server key"),
+ OldPassword: "old password",
+ StateInfo: &state.Info{
+ Addrs: []string{"foo.com:355", "bar:545"},
+ CACert: []byte("ca cert"),
+ Tag: "entity",
+ Password: "current password",
+ },
+ APIInfo: &api.Info{
+ Addrs: []string{"foo.com:555"},
+ CACert: []byte("api ca cert"),
+ },
+ },
+ checkErr: "API entity tag not found in configuration",
+}, {
+ about: "mismatched entity tags",
+ conf: agent.Conf{
+ StateServerCert: []byte("server cert"),
+ StateServerKey: []byte("server key"),
+ OldPassword: "old password",
+ StateInfo: &state.Info{
+ Addrs: []string{"foo.com:355", "bar:545"},
+ CACert: []byte("ca cert"),
+ Tag: "entity",
+ Password: "current password",
+ },
+ APIInfo: &api.Info{
+ Tag: "other",
+ Addrs: []string{"foo.com:555"},
+ CACert: []byte("api ca cert"),
+ },
+ },
+ checkErr: "mismatched entity tags",
+}, {
+ about: "no state entity tag",
conf: agent.Conf{
OldPassword: "old password",
StateInfo: &state.Info{
@@ -121,15 +121,15 @@
Password: "current password",
},
},
- checkErr: "state entity name not found in configuration",
+ checkErr: "state entity tag not found in configuration",
}, {
about: "no state server address",
conf: agent.Conf{
OldPassword: "old password",
StateInfo: &state.Info{
- CACert: []byte("ca cert"),
- Password: "current password",
- EntityName: "entity",
+ CACert: []byte("ca cert"),
+ Password: "current password",
+ Tag: "entity",
},
},
checkErr: "state server address not found in configuration",
@@ -138,10 +138,10 @@
conf: agent.Conf{
OldPassword: "old password",
StateInfo: &state.Info{
- Addrs: []string{"foo"},
- CACert: []byte("ca cert"),
- EntityName: "entity",
- Password: "current password",
+ Addrs: []string{"foo"},
+ CACert: []byte("ca cert"),
+ Tag: "entity",
+ Password: "current password",
},
},
checkErr: "invalid state server address \"foo\"",
@@ -150,10 +150,10 @@
conf: agent.Conf{
OldPassword: "old password",
StateInfo: &state.Info{
- Addrs: []string{"foo:bar"},
- CACert: []byte("ca cert"),
- EntityName: "entity",
- Password: "current password",
+ Addrs: []string{"foo:bar"},
+ CACert: []byte("ca cert"),
+ Tag: "entity",
+ Password: "current password",
},
},
checkErr: "invalid state server address \"foo:bar\"",
@@ -162,10 +162,10 @@
conf: agent.Conf{
OldPassword: "old password",
StateInfo: &state.Info{
- Addrs: []string{"foo:345d"},
- CACert: []byte("ca cert"),
- EntityName: "entity",
- Password: "current password",
+ Addrs: []string{"foo:345d"},
+ CACert: []byte("ca cert"),
+ Tag: "entity",
+ Password: "current password",
},
},
checkErr: "invalid state server address \"foo:345d\"",
@@ -174,15 +174,15 @@
conf: agent.Conf{
OldPassword: "old password",
StateInfo: &state.Info{
- Addrs: []string{"foo:345"},
- CACert: []byte("ca cert"),
- EntityName: "entity",
- Password: "current password",
+ Addrs: []string{"foo:345"},
+ CACert: []byte("ca cert"),
+ Tag: "entity",
+ Password: "current password",
},
APIInfo: &api.Info{
- EntityName: "entity",
- Addrs: []string{"bar.com:455", "foo"},
- CACert: []byte("ca cert"),
+ Tag: "entity",
+ Addrs: []string{"bar.com:455", "foo"},
+ CACert: []byte("ca cert"),
},
},
checkErr: "invalid API server address \"foo\"",
@@ -191,15 +191,15 @@
conf: agent.Conf{
OldPassword: "old password",
StateInfo: &state.Info{
- Addrs: []string{"foo:345"},
- CACert: []byte("ca cert"),
- EntityName: "entity",
- Password: "current password",
+ Addrs: []string{"foo:345"},
+ CACert: []byte("ca cert"),
+ Tag: "entity",
+ Password: "current password",
},
APIInfo: &api.Info{
- EntityName: "entity",
- Addrs: []string{"foo:3"},
- CACert: []byte{},
+ Tag: "entity",
+ Addrs: []string{"foo:3"},
+ CACert: []byte{},
},
},
checkErr: "API CA certficate not found in configuration",
@@ -248,12 +248,12 @@
rconf, err := agent.ReadConf(dataDir, "another")
c.Assert(err, IsNil)
- c.Assert(rconf.StateInfo.EntityName, Equals, "another")
+ c.Assert(rconf.StateInfo.Tag, Equals, "another")
if rconf.StateInfo != nil {
- rconf.StateInfo.EntityName = conf.EntityName()
+ rconf.StateInfo.Tag = conf.Tag()
}
if rconf.APIInfo != nil {
- rconf.APIInfo.EntityName = conf.EntityName()
+ rconf.APIInfo.Tag = conf.Tag()
}
c.Assert(rconf, DeepEquals, &conf)
@@ -271,7 +271,7 @@
c.Assert(err, IsNil)
c.Assert(info.Mode()&os.ModePerm, Equals, os.FileMode(0600))
- rconf, err = agent.ReadConf(dataDir, conf.StateInfo.EntityName)
+ rconf, err = agent.ReadConf(dataDir, conf.StateInfo.Tag)
c.Assert(err, IsNil)
c.Assert(rconf, DeepEquals, &conf)
@@ -284,10 +284,10 @@
func (suite) TestCheckNoDataDir(c *C) {
conf := agent.Conf{
StateInfo: &state.Info{
- Addrs: []string{"x:4"},
- CACert: []byte("xxx"),
- EntityName: "bar",
- Password: "pass",
+ Addrs: []string{"x:4"},
+ CACert: []byte("xxx"),
+ Tag: "bar",
+ Password: "pass",
},
}
c.Assert(conf.Check(), ErrorMatches, "data directory not found in configuration")
@@ -297,10 +297,10 @@
conf := agent.Conf{
DataDir: "/foo",
StateInfo: &state.Info{
- Addrs: []string{"x:4"},
- CACert: []byte("xxx"),
- EntityName: "bar",
- Password: "pass",
+ Addrs: []string{"x:4"},
+ CACert: []byte("xxx"),
+ Tag: "bar",
+ Password: "pass",
},
}
c.Assert(conf.Dir(), Equals, "/foo/agents/bar")
@@ -310,10 +310,10 @@
conf := agent.Conf{
DataDir: "/foo",
StateInfo: &state.Info{
- Addrs: []string{"x:4"},
- CACert: []byte("xxx"),
- EntityName: "bar",
- Password: "pass",
+ Addrs: []string{"x:4"},
+ CACert: []byte("xxx"),
+ Tag: "bar",
+ Password: "pass",
},
}
c.Assert(conf.File("x/y"), Equals, "/foo/agents/bar/x/y")
=== modified file 'environs/agent/tools.go'
--- environs/agent/tools.go 2013-02-25 16:46:55 +0000
+++ environs/agent/tools.go 2013-04-08 08:09:22 +0000
@@ -11,7 +11,6 @@
"launchpad.net/juju-core/version"
"os"
"path"
- "path/filepath"
"strings"
)
@@ -75,12 +74,12 @@
if hdr.Typeflag != tar.TypeReg {
return fmt.Errorf("bad file type %c in file %q in tools archive", hdr.Typeflag, hdr.Name)
}
- name := filepath.Join(dir, hdr.Name)
+ name := path.Join(dir, hdr.Name)
if err := writeFile(name, os.FileMode(hdr.Mode&0777), tr); err != nil {
return fmt.Errorf("tar extract %q failed: %v", name, err)
}
}
- err = ioutil.WriteFile(filepath.Join(dir, urlFile), []byte(tools.URL), 0644)
+ err = ioutil.WriteFile(path.Join(dir, urlFile), []byte(tools.URL), 0644)
if err != nil {
return err
}
@@ -102,7 +101,7 @@
if err == nil || os.IsNotExist(err) {
return
}
- log.Printf("environs: cannot remove %q: %v", dir, err)
+ log.Warningf("environs: cannot remove %q: %v", dir, err)
}
func writeFile(name string, mode os.FileMode, r io.Reader) error {
@@ -119,7 +118,7 @@
// in the dataDir directory, and returns a Tools instance describing them.
func ReadTools(dataDir string, vers version.Binary) (*state.Tools, error) {
dir := SharedToolsDir(dataDir, vers)
- urlData, err := ioutil.ReadFile(filepath.Join(dir, urlFile))
+ urlData, err := ioutil.ReadFile(path.Join(dir, urlFile))
if err != nil {
return nil, fmt.Errorf("cannot read URL in tools directory: %v", err)
}
=== modified file 'environs/boilerplate_config.go'
--- environs/boilerplate_config.go 2013-02-20 03:57:12 +0000
+++ environs/boilerplate_config.go 2013-04-08 08:09:22 +0000
@@ -20,6 +20,10 @@
## Values in <brackets> below need to be filled in by the user.
+## The default environment is chosen when one is not specified using either:
+## -e, --environment command line parameter
+## JUJU_ENV environment variable
+## If both -e and JUJU_ENV are specified, the command line parameter has precedence.
default: amazon
environments:
=== modified file 'environs/boilerplate_config_test.go'
--- environs/boilerplate_config_test.go 2013-02-04 01:20:55 +0000
+++ environs/boilerplate_config_test.go 2013-04-08 08:09:22 +0000
@@ -3,6 +3,7 @@
import (
. "launchpad.net/gocheck"
"launchpad.net/juju-core/environs"
+ "launchpad.net/juju-core/environs/config"
_ "launchpad.net/juju-core/environs/ec2"
_ "launchpad.net/juju-core/environs/openstack"
)
@@ -13,6 +14,7 @@
var _ = Suite(&BoilerplateConfigSuite{})
func (*BoilerplateConfigSuite) TestBoilerPlateGeneration(c *C) {
+ defer config.SetJujuHome(config.SetJujuHome(c.MkDir()))
boilerplate_text := environs.BoilerplateConfig()
_, err := environs.ReadEnvironsBytes([]byte(boilerplate_text))
c.Assert(err, IsNil)
=== modified file 'environs/bootstrap.go'
--- environs/bootstrap.go 2012-11-30 00:19:13 +0000
+++ environs/bootstrap.go 2013-04-08 08:09:22 +0000
@@ -2,52 +2,23 @@
import (
"fmt"
- "io/ioutil"
"launchpad.net/juju-core/cert"
- "launchpad.net/juju-core/environs/config"
- "os"
- "path/filepath"
+ "launchpad.net/juju-core/constraints"
"time"
)
-// Bootstrap bootstraps the given environment. If the environment does
-// not contain a CA certificate, a new certificate and key pair are
-// generated, added to the environment configuration, and writeCertAndKey
-// will be called to save them. If writeCertFile is nil, the generated
-// certificate and key will be saved to ~/.juju/<environ-name>-cert.pem
-// and ~/.juju/<environ-name>-private-key.pem.
-//
-// If uploadTools is true, the current version of the juju tools will be
-// uploaded, as documented in Environ.Bootstrap.
-func Bootstrap(environ Environ, uploadTools bool, writeCertAndKey func(environName string, cert, key []byte) error) error {
- if writeCertAndKey == nil {
- writeCertAndKey = writeCertAndKeyToHome
- }
+// Bootstrap bootstraps the given environment. The supplied constraints are
+// used to provision the instance, and are also set within the bootstrapped
+// environment.
+func Bootstrap(environ Environ, cons constraints.Value) error {
cfg := environ.Config()
caCert, hasCACert := cfg.CACert()
caKey, hasCAKey := cfg.CAPrivateKey()
if !hasCACert {
- if hasCAKey {
- return fmt.Errorf("environment configuration with CA private key but no certificate")
- }
- var err error
- caCert, caKey, err = cert.NewCA(environ.Name(), time.Now().UTC().AddDate(10, 0, 0))
- if err != nil {
- return err
- }
- m := cfg.AllAttrs()
- m["ca-cert"] = string(caCert)
- m["ca-private-key"] = string(caKey)
- cfg, err = config.New(m)
- if err != nil {
- return fmt.Errorf("cannot create environment configuration with new CA: %v", err)
- }
- if err := environ.SetConfig(cfg); err != nil {
- return fmt.Errorf("cannot set environment configuration with CA: %v", err)
- }
- if err := writeCertAndKey(environ.Name(), caCert, caKey); err != nil {
- return fmt.Errorf("cannot write CA certificate and key: %v", err)
- }
+ return fmt.Errorf("environment configuration missing CA certificate")
+ }
+ if !hasCAKey {
+ return fmt.Errorf("environment configuration missing CA private key")
}
// Generate a new key pair and certificate for
// the newly bootstrapped instance.
@@ -55,16 +26,5 @@
if err != nil {
return fmt.Errorf("cannot generate bootstrap certificate: %v", err)
}
- return environ.Bootstrap(uploadTools, cert, key)
-}
-
-func writeCertAndKeyToHome(name string, cert, key []byte) error {
- path := filepath.Join(os.Getenv("HOME"), ".juju", name)
- if err := ioutil.WriteFile(path+"-cert.pem", cert, 0644); err != nil {
- return err
- }
- if err := ioutil.WriteFile(path+"-private-key.pem", key, 0600); err != nil {
- return err
- }
- return nil
+ return environ.Bootstrap(cons, cert, key)
}
=== modified file 'environs/bootstrap_test.go'
--- environs/bootstrap_test.go 2012-11-27 18:44:55 +0000
+++ environs/bootstrap_test.go 2013-04-08 08:09:22 +0000
@@ -5,16 +5,20 @@
"io/ioutil"
. "launchpad.net/gocheck"
"launchpad.net/juju-core/cert"
+ "launchpad.net/juju-core/constraints"
"launchpad.net/juju-core/environs"
"launchpad.net/juju-core/environs/config"
"launchpad.net/juju-core/testing"
- "os"
- "path/filepath"
"time"
)
+const (
+ useDefaultKeys = true
+ noKeysDefined = false
+)
+
type bootstrapSuite struct {
- oldHome string
+ home *testing.FakeHome
testing.LoggingSuite
}
@@ -22,106 +26,49 @@
func (s *bootstrapSuite) SetUpTest(c *C) {
s.LoggingSuite.SetUpTest(c)
- s.oldHome = os.Getenv("HOME")
- home := c.MkDir()
- os.Setenv("HOME", home)
- err := os.Mkdir(filepath.Join(home, ".juju"), 0777)
- c.Assert(err, IsNil)
+ s.home = testing.MakeFakeHomeNoEnvironments(c, "foo")
}
func (s *bootstrapSuite) TearDownTest(c *C) {
- os.Setenv("HOME", s.oldHome)
+ s.home.Restore()
+}
+
+func (s *bootstrapSuite) TestBootstrapNeedsConfigCert(c *C) {
+ env := newEnviron("bar", noKeysDefined)
+ err := environs.Bootstrap(env, constraints.Value{})
+ c.Assert(err, ErrorMatches, "environment configuration missing CA certificate")
}
func (s *bootstrapSuite) TestBootstrapKeyGeneration(c *C) {
- env := newEnviron("foo", nil, nil)
- err := environs.Bootstrap(env, false, nil)
- c.Assert(err, IsNil)
- c.Assert(env.bootstrapCount, Equals, 1)
- _, _, err = cert.ParseCertAndKey(env.certPEM, env.keyPEM)
- c.Assert(err, IsNil)
-
- // Check that the generated CA key has been written correctly.
- caCertPEM, err := ioutil.ReadFile(filepath.Join(os.Getenv("HOME"), ".juju", "foo-cert.pem"))
- c.Assert(err, IsNil)
- caKeyPEM, err := ioutil.ReadFile(filepath.Join(os.Getenv("HOME"), ".juju", "foo-private-key.pem"))
- c.Assert(err, IsNil)
-
- // Check that the cert and key have been set correctly in the configuration
- cfgCertPEM, cfgCertOK := env.cfg.CACert()
- cfgKeyPEM, cfgKeyOK := env.cfg.CAPrivateKey()
- c.Assert(cfgCertOK, Equals, true)
- c.Assert(cfgKeyOK, Equals, true)
- c.Assert(cfgCertPEM, DeepEquals, caCertPEM)
- c.Assert(cfgKeyPEM, DeepEquals, caKeyPEM)
-
- caCert, _, err := cert.ParseCertAndKey(cfgCertPEM, cfgKeyPEM)
- c.Assert(err, IsNil)
- c.Assert(caCert.Subject.CommonName, Equals, `juju-generated CA for environment foo`)
-
- verifyCert(c, env.certPEM, caCertPEM)
-}
-
-func verifyCert(c *C, srvCertPEM, caCertPEM []byte) {
- err := cert.Verify(srvCertPEM, caCertPEM, time.Now())
- c.Assert(err, IsNil)
- err = cert.Verify(srvCertPEM, caCertPEM, time.Now().AddDate(9, 0, 0))
- c.Assert(err, IsNil)
-}
-
-func (s *bootstrapSuite) TestBootstrapFuncKeyGeneration(c *C) {
- env := newEnviron("foo", nil, nil)
- var savedCert, savedKey []byte
- err := environs.Bootstrap(env, false, func(name string, cert, key []byte) error {
- savedCert = cert
- savedKey = key
- return nil
- })
- c.Assert(err, IsNil)
- c.Assert(env.bootstrapCount, Equals, 1)
- _, _, err = cert.ParseCertAndKey(env.certPEM, env.keyPEM)
- c.Assert(err, IsNil)
-
- // Check that the cert and key have been set correctly in the configuration
- cfgCertPEM, cfgCertOK := env.cfg.CACert()
- cfgKeyPEM, cfgKeyOK := env.cfg.CAPrivateKey()
- c.Assert(cfgCertOK, Equals, true)
- c.Assert(cfgKeyOK, Equals, true)
- c.Assert(cfgCertPEM, DeepEquals, savedCert)
- c.Assert(cfgKeyPEM, DeepEquals, savedKey)
-
- caCert, _, err := cert.ParseCertAndKey(cfgCertPEM, cfgKeyPEM)
- c.Assert(err, IsNil)
- c.Assert(caCert.Subject.CommonName, Equals, `juju-generated CA for environment foo`)
-
- verifyCert(c, env.certPEM, cfgCertPEM)
-}
-
-func panicWrite(name string, cert, key []byte) error {
- panic("writeCertAndKey called unexpectedly")
-}
-
-func (s *bootstrapSuite) TestBootstrapExistingKey(c *C) {
- env := newEnviron("foo", []byte(testing.CACert), []byte(testing.CAKey))
- err := environs.Bootstrap(env, false, panicWrite)
- c.Assert(err, IsNil)
- c.Assert(env.bootstrapCount, Equals, 1)
-
- verifyCert(c, env.certPEM, []byte(testing.CACert))
-}
-
-func (s *bootstrapSuite) TestBootstrapUploadTools(c *C) {
- env := newEnviron("foo", nil, nil)
- err := environs.Bootstrap(env, false, nil)
- c.Assert(err, IsNil)
- c.Assert(env.bootstrapCount, Equals, 1)
- c.Assert(env.uploadTools, Equals, false)
-
- env = newEnviron("foo", nil, nil)
- err = environs.Bootstrap(env, true, nil)
- c.Assert(err, IsNil)
- c.Assert(env.bootstrapCount, Equals, 1)
- c.Assert(env.uploadTools, Equals, true)
+ env := newEnviron("foo", useDefaultKeys)
+ err := environs.Bootstrap(env, constraints.Value{})
+ c.Assert(err, IsNil)
+ c.Assert(env.bootstrapCount, Equals, 1)
+
+ caCertPEM, err := ioutil.ReadFile(config.JujuHomePath("foo-cert.pem"))
+ c.Assert(err, IsNil)
+
+ err = cert.Verify(env.certPEM, caCertPEM, time.Now())
+ c.Assert(err, IsNil)
+ err = cert.Verify(env.certPEM, caCertPEM, time.Now().AddDate(9, 0, 0))
+ c.Assert(err, IsNil)
+}
+
+func (s *bootstrapSuite) TestBootstrapEmptyConstraints(c *C) {
+ env := newEnviron("foo", useDefaultKeys)
+ err := environs.Bootstrap(env, constraints.Value{})
+ c.Assert(err, IsNil)
+ c.Assert(env.bootstrapCount, Equals, 1)
+ c.Assert(env.constraints, DeepEquals, constraints.Value{})
+}
+
+func (s *bootstrapSuite) TestBootstrapSpecifiedConstraints(c *C) {
+ env := newEnviron("foo", useDefaultKeys)
+ cons := constraints.MustParse("cpu-cores=2 mem=4G")
+ err := environs.Bootstrap(env, cons)
+ c.Assert(err, IsNil)
+ c.Assert(env.bootstrapCount, Equals, 1)
+ c.Assert(env.constraints, DeepEquals, cons)
}
type bootstrapEnviron struct {
@@ -131,12 +78,12 @@
// The following fields are filled in when Bootstrap is called.
bootstrapCount int
- uploadTools bool
+ constraints constraints.Value
certPEM []byte
keyPEM []byte
}
-func newEnviron(name string, caCertPEM, caKeyPEM []byte) *bootstrapEnviron {
+func newEnviron(name string, defaultKeys bool) *bootstrapEnviron {
m := map[string]interface{}{
"name": name,
"type": "test",
@@ -144,11 +91,9 @@
"ca-cert": "",
"ca-private-key": "",
}
- if caCertPEM != nil {
- m["ca-cert"] = string(caCertPEM)
- }
- if caKeyPEM != nil {
- m["ca-private-key"] = string(caKeyPEM)
+ if defaultKeys {
+ m["ca-cert"] = testing.CACert
+ m["ca-private-key"] = testing.CAKey
}
cfg, err := config.New(m)
if err != nil {
@@ -164,9 +109,9 @@
return e.name
}
-func (e *bootstrapEnviron) Bootstrap(uploadTools bool, certPEM, keyPEM []byte) error {
+func (e *bootstrapEnviron) Bootstrap(cons constraints.Value, certPEM, keyPEM []byte) error {
e.bootstrapCount++
- e.uploadTools = uploadTools
+ e.constraints = cons
e.certPEM = certPEM
e.keyPEM = keyPEM
return nil
=== added file 'environs/cert.go'
--- environs/cert.go 1970-01-01 00:00:00 +0000
+++ environs/cert.go 2013-04-08 08:09:22 +0000
@@ -0,0 +1,73 @@
+package environs
+
+import (
+ "fmt"
+ "io/ioutil"
+ "launchpad.net/juju-core/cert"
+ "launchpad.net/juju-core/environs/config"
+ "os"
+ "path/filepath"
+ "time"
+)
+
+type CreatedCert bool
+
+const (
+ CertCreated CreatedCert = true
+ CertExists CreatedCert = false
+)
+
+func WriteCertAndKeyToHome(name string, cert, key []byte) error {
+ // If the $HOME/.juju directory doesn't exist, create it.
+ jujuDir := filepath.Join(os.Getenv("HOME"), ".juju")
+ if err := os.MkdirAll(jujuDir, 0775); err != nil {
+ return err
+ }
+ path := filepath.Join(jujuDir, name)
+ if err := ioutil.WriteFile(path+"-cert.pem", cert, 0644); err != nil {
+ return err
+ }
+ return ioutil.WriteFile(path+"-private-key.pem", key, 0600)
+}
+
+func generateCertificate(environ Environ, writeCertAndKey func(environName string, cert, key []byte) error) error {
+ cfg := environ.Config()
+ caCert, caKey, err := cert.NewCA(environ.Name(), time.Now().UTC().AddDate(10, 0, 0))
+ if err != nil {
+ return err
+ }
+ m := cfg.AllAttrs()
+ m["ca-cert"] = string(caCert)
+ m["ca-private-key"] = string(caKey)
+ cfg, err = config.New(m)
+ if err != nil {
+ return fmt.Errorf("cannot create environment configuration with new CA: %v", err)
+ }
+ if err := environ.SetConfig(cfg); err != nil {
+ return fmt.Errorf("cannot set environment configuration with CA: %v", err)
+ }
+ if err := writeCertAndKey(environ.Name(), caCert, caKey); err != nil {
+ return fmt.Errorf("cannot write CA certificate and key: %v", err)
+ }
+ return nil
+}
+
+// EnsureCertificate makes sure that there is a certificate and private key
+// for the specified environment. If one does not exist, then a certificate
+// is generated.
+func EnsureCertificate(environ Environ, writeCertAndKey func(environName string, cert, key []byte) error) (CreatedCert, error) {
+ cfg := environ.Config()
+ _, hasCACert := cfg.CACert()
+ _, hasCAKey := cfg.CAPrivateKey()
+
+ if hasCACert && hasCAKey {
+ // All is good in the world.
+ return CertExists, nil
+ }
+ // It is not possible to create an environment that has a private key, but no certificate.
+ if hasCACert && !hasCAKey {
+ return CertExists, fmt.Errorf("environment configuration with a certificate but no CA private key")
+ }
+
+ return CertCreated, generateCertificate(environ, writeCertAndKey)
+}
=== added file 'environs/cert_internal_test.go'
--- environs/cert_internal_test.go 1970-01-01 00:00:00 +0000
+++ environs/cert_internal_test.go 2013-04-08 08:09:22 +0000
@@ -0,0 +1,46 @@
+package environs
+
+import (
+ . "launchpad.net/gocheck"
+ "launchpad.net/juju-core/cert"
+ "launchpad.net/juju-core/testing"
+)
+
+// EnvironsCertSuite tests the internal functions defined in environs/cert.go
+type EnvironsCertSuite struct {
+ testing.LoggingSuite
+}
+
+var _ = Suite(&EnvironsCertSuite{})
+
+type testCerts struct {
+ cert []byte
+ key []byte
+}
+
+func (*EnvironsCertSuite) TestGenerateCertificate(c *C) {
+ defer testing.MakeSampleHome(c).Restore()
+ env, err := NewFromName(testing.SampleEnvName)
+ c.Assert(err, IsNil)
+
+ var savedCerts testCerts
+ writeFunc := func(name string, cert, key []byte) error {
+ savedCerts.cert = cert
+ savedCerts.key = key
+ return nil
+ }
+ generateCertificate(env, writeFunc)
+
+ // Check that the cert and key have been set correctly in the configuration
+ cfgCertPEM, cfgCertOK := env.Config().CACert()
+ cfgKeyPEM, cfgKeyOK := env.Config().CAPrivateKey()
+ c.Assert(cfgCertOK, Equals, true)
+ c.Assert(cfgKeyOK, Equals, true)
+ c.Assert(cfgCertPEM, DeepEquals, savedCerts.cert)
+ c.Assert(cfgKeyPEM, DeepEquals, savedCerts.key)
+
+ // Check the common name of the generated cert
+ caCert, _, err := cert.ParseCertAndKey(cfgCertPEM, cfgKeyPEM)
+ c.Assert(err, IsNil)
+ c.Assert(caCert.Subject.CommonName, Equals, `juju-generated CA for environment `+testing.SampleEnvName)
+}
=== added file 'environs/cert_test.go'
--- environs/cert_test.go 1970-01-01 00:00:00 +0000
+++ environs/cert_test.go 2013-04-08 08:09:22 +0000
@@ -0,0 +1,80 @@
+package environs_test
+
+import (
+ "io/ioutil"
+ . "launchpad.net/gocheck"
+ "launchpad.net/juju-core/environs"
+ "launchpad.net/juju-core/testing"
+)
+
+type EnvironsCertSuite struct {
+ testing.LoggingSuite
+}
+
+var _ = Suite(&EnvironsCertSuite{})
+
+func (*EnvironsCertSuite) TestWriteCertAndKeyToHome(c *C) {
+ defer testing.MakeEmptyFakeHome(c).Restore()
+
+ cert := []byte("a cert")
+ key := []byte("a key")
+ err := environs.WriteCertAndKeyToHome("foo", cert, key)
+ c.Assert(err, IsNil)
+
+ // Check that the generated CA key has been written correctly.
+ caCertPEM, err := ioutil.ReadFile(testing.HomePath(".juju", "foo-cert.pem"))
+ c.Assert(err, IsNil)
+ c.Assert(string(caCertPEM), Equals, "a cert")
+
+ caKeyPEM, err := ioutil.ReadFile(testing.HomePath(".juju", "foo-private-key.pem"))
+ c.Assert(err, IsNil)
+ c.Assert(string(caKeyPEM), Equals, "a key")
+}
+
+func (*EnvironsCertSuite) TestEnsureCertificateMissingKey(c *C) {
+ defer testing.MakeFakeHome(c, testing.SingleEnvConfig).Restore()
+ envName := testing.SampleEnvName
+
+ keyPath := testing.HomePath(".juju", envName+"-cert.pem")
+ ioutil.WriteFile(keyPath, []byte(testing.CACert), 0600)
+
+ // Need to create the environment after the cert has been written.
+ env, err := environs.NewFromName(envName)
+ c.Assert(err, IsNil)
+
+ writeCalled := false
+ _, err = environs.EnsureCertificate(env, func(name string, cert, key []byte) error {
+ writeCalled = true
+ return nil
+ })
+ c.Assert(err, ErrorMatches, "environment configuration with a certificate but no CA private key")
+ c.Assert(writeCalled, Equals, false)
+}
+
+func (*EnvironsCertSuite) TestEnsureCertificateExisting(c *C) {
+ defer testing.MakeSampleHome(c).Restore()
+ env, err := environs.NewFromName(testing.SampleEnvName)
+ c.Assert(err, IsNil)
+ writeCalled := false
+ created, err := environs.EnsureCertificate(env, func(name string, cert, key []byte) error {
+ writeCalled = true
+ return nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(created, Equals, environs.CertExists)
+ c.Assert(writeCalled, Equals, false)
+}
+
+func (*EnvironsCertSuite) TestEnsureCertificate(c *C) {
+ defer testing.MakeFakeHome(c, testing.SingleEnvConfig).Restore()
+ env, err := environs.NewFromName(testing.SampleEnvName)
+ c.Assert(err, IsNil)
+ writeCalled := false
+ created, err := environs.EnsureCertificate(env, func(name string, cert, key []byte) error {
+ writeCalled = true
+ return nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(created, Equals, environs.CertCreated)
+ c.Assert(writeCalled, Equals, true)
+}
=== modified file 'environs/cloudinit/cloudinit.go'
--- environs/cloudinit/cloudinit.go 2013-04-05 10:55:10 +0000
+++ environs/cloudinit/cloudinit.go 2013-04-08 08:09:22 +0000
@@ -5,9 +5,11 @@
"fmt"
"launchpad.net/goyaml"
"launchpad.net/juju-core/cloudinit"
+ "launchpad.net/juju-core/constraints"
"launchpad.net/juju-core/environs/agent"
"launchpad.net/juju-core/environs/config"
"launchpad.net/juju-core/log"
+ "launchpad.net/juju-core/log/syslog"
"launchpad.net/juju-core/state"
"launchpad.net/juju-core/state/api"
"launchpad.net/juju-core/trivial"
@@ -74,6 +76,9 @@
// Config holds the initial environment configuration.
Config *config.Config
+
+ // Constraints holds the initial environment constraints.
+ Constraints constraints.Value
}
func addScripts(c *cloudinit.Config, scripts ...string) {
@@ -122,6 +127,7 @@
debugFlag = " --debug"
}
+ var syslogConfigRenderer syslog.SyslogConfigRenderer
if cfg.StateServer {
certKey := string(cfg.StateServerCert) + string(cfg.StateServerKey)
addFile(c, cfg.dataFile("server.pem"), certKey, 0600)
@@ -143,13 +149,28 @@
cfg.jujuTools()+"/jujud bootstrap-state"+
" --data-dir "+shquote(cfg.DataDir)+
" --env-config "+shquote(base64yaml(cfg.Config))+
+ " --constraints "+shquote(cfg.Constraints.String())+
debugFlag,
"rm -rf "+shquote(acfg.Dir()),
)
- }
+ syslogConfigRenderer = syslog.NewAccumulateConfig(
+ state.MachineTag(cfg.MachineId))
+ } else {
+ syslogConfigRenderer = syslog.NewForwardConfig(
+ state.MachineTag(cfg.MachineId), cfg.stateHostAddrs())
+ }
+
+ content, err := syslogConfigRenderer.Render()
+ if err != nil {
+ return nil, err
+ }
+ addScripts(c,
+ fmt.Sprintf("cat > /etc/rsyslog.d/25-juju.conf << 'EOF'\n%sEOF\n", string(content)),
+ )
+ c.AddRunCmd("restart rsyslog")
if _, err := addAgentToBoot(c, cfg, "machine",
- state.MachineEntityName(cfg.MachineId),
+ state.MachineTag(cfg.MachineId),
fmt.Sprintf("--machine-id %s "+debugFlag, cfg.MachineId)); err != nil {
return nil, err
}
@@ -173,7 +194,7 @@
return path.Join(cfg.DataDir, name)
}
-func (cfg *MachineConfig) agentConfig(entityName string) *agent.Conf {
+func (cfg *MachineConfig) agentConfig(tag string) *agent.Conf {
info := *cfg.StateInfo
apiInfo := *cfg.APIInfo
c := &agent.Conf{
@@ -186,12 +207,12 @@
APIPort: cfg.APIPort,
}
c.StateInfo.Addrs = cfg.stateHostAddrs()
- c.StateInfo.EntityName = entityName
+ c.StateInfo.Tag = tag
c.StateInfo.Password = ""
c.OldPassword = cfg.StateInfo.Password
c.APIInfo.Addrs = cfg.apiHostAddrs()
- c.APIInfo.EntityName = entityName
+ c.APIInfo.Tag = tag
c.APIInfo.Password = ""
return c
@@ -199,8 +220,8 @@
// addAgentInfo adds agent-required information to the agent's directory
// and returns the agent directory name.
-func addAgentInfo(c *cloudinit.Config, cfg *MachineConfig, entityName string) (*agent.Conf, error) {
- acfg := cfg.agentConfig(entityName)
+func addAgentInfo(c *cloudinit.Config, cfg *MachineConfig, tag string) (*agent.Conf, error) {
+ acfg := cfg.agentConfig(tag)
cmds, err := acfg.WriteCommands()
if err != nil {
return nil, err
@@ -209,8 +230,8 @@
return acfg, nil
}
-func addAgentToBoot(c *cloudinit.Config, cfg *MachineConfig, kind, entityName, args string) (*agent.Conf, error) {
- acfg, err := addAgentInfo(c, cfg, entityName)
+func addAgentToBoot(c *cloudinit.Config, cfg *MachineConfig, kind, tag, args string) (*agent.Conf, error) {
+ acfg, err := addAgentInfo(c, cfg, tag)
if err != nil {
return nil, err
}
@@ -218,12 +239,12 @@
// Make the agent run via a symbolic link to the actual tools
// directory, so it can upgrade itself without needing to change
// the upstart script.
- toolsDir := agent.ToolsDir(cfg.DataDir, entityName)
+ toolsDir := agent.ToolsDir(cfg.DataDir, tag)
// TODO(dfc) ln -nfs, so it doesn't fail if for some reason that the target already exists
addScripts(c, fmt.Sprintf("ln -s %v %s", cfg.Tools.Binary, shquote(toolsDir)))
- svc := upstart.NewService("jujud-" + entityName)
- logPath := fmt.Sprintf("/var/log/juju/%s.log", entityName)
+ svc := upstart.NewService("jujud-" + tag)
+ logPath := fmt.Sprintf("/var/log/juju/%s.log", tag)
cmd := fmt.Sprintf(
"%s/jujud %s"+
" --log-file %s"+
@@ -236,13 +257,13 @@
)
conf := &upstart.Conf{
Service: *svc,
- Desc: fmt.Sprintf("juju %s agent", entityName),
+ Desc: fmt.Sprintf("juju %s agent", tag),
Cmd: cmd,
Out: logPath,
}
cmds, err := conf.InstallCommands()
if err != nil {
- return nil, fmt.Errorf("cannot make cloud-init upstart script for the %s agent: %v", entityName, err)
+ return nil, fmt.Errorf("cannot make cloud-init upstart script for the %s agent: %v", tag, err)
}
addScripts(c, cmds...)
return acfg, nil
@@ -354,11 +375,11 @@
if cfg.Config == nil {
return fmt.Errorf("missing environment configuration")
}
- if cfg.StateInfo.EntityName != "" {
- return fmt.Errorf("entity name must be blank when starting a state server")
+ if cfg.StateInfo.Tag != "" {
+ return fmt.Errorf("entity tag must be blank when starting a state server")
}
- if cfg.APIInfo.EntityName != "" {
- return fmt.Errorf("entity name must be blank when starting a state server")
+ if cfg.APIInfo.Tag != "" {
+ return fmt.Errorf("entity tag must be blank when starting a state server")
}
if len(cfg.StateServerCert) == 0 {
return fmt.Errorf("missing state server certificate")
@@ -376,14 +397,14 @@
if len(cfg.StateInfo.Addrs) == 0 {
return fmt.Errorf("missing state hosts")
}
- if cfg.StateInfo.EntityName != state.MachineEntityName(cfg.MachineId) {
- return fmt.Errorf("entity name must match started machine")
+ if cfg.StateInfo.Tag != state.MachineTag(cfg.MachineId) {
+ return fmt.Errorf("entity tag must match started machine")
}
if len(cfg.APIInfo.Addrs) == 0 {
return fmt.Errorf("missing API hosts")
}
- if cfg.APIInfo.EntityName != state.MachineEntityName(cfg.MachineId) {
- return fmt.Errorf("entity name must match started machine")
+ if cfg.APIInfo.Tag != state.MachineTag(cfg.MachineId) {
+ return fmt.Errorf("entity tag must match started machine")
}
}
return nil
=== modified file 'environs/cloudinit/cloudinit_test.go'
--- environs/cloudinit/cloudinit_test.go 2013-04-05 10:55:10 +0000
+++ environs/cloudinit/cloudinit_test.go 2013-04-08 08:09:22 +0000
@@ -4,7 +4,11 @@
"encoding/base64"
. "launchpad.net/gocheck"
"launchpad.net/goyaml"
+<<<<<<< TREE
cloudinit_core "launchpad.net/juju-core/cloudinit"
+=======
+ "launchpad.net/juju-core/constraints"
+>>>>>>> MERGE-SOURCE
"launchpad.net/juju-core/environs/cloudinit"
"launchpad.net/juju-core/environs/config"
"launchpad.net/juju-core/state"
@@ -23,27 +27,27 @@
var _ = Suite(&cloudinitSuite{})
-var envConfig = mustNewConfig(map[string]interface{}{
- "type": "ec2",
- "name": "foo",
- "default-series": "series",
- "authorized-keys": "keys",
- "ca-cert": testing.CACert,
-})
-
-func mustNewConfig(m map[string]interface{}) *config.Config {
- cfg, err := config.New(m)
- if err != nil {
- panic(err)
- }
- return cfg
-}
+var envConstraints = constraints.MustParse("mem=2G")
type cloudinitTest struct {
cfg cloudinit.MachineConfig
+ setEnvConfig bool
expectScripts string
}
+func minimalConfig(c *C) *config.Config {
+ cfg, err := config.New(map[string]interface{}{
+ "type": "test",
+ "name": "test-name",
+ "default-series": "test-series",
+ "authorized-keys": "test-keys",
+ "ca-cert": testing.CACert,
+ "ca-private-key": "",
+ })
+ c.Assert(err, IsNil)
+ return cfg
+}
+
// Each test gives a cloudinit config - we check the
// output to see if it looks correct.
var cloudinitTests = []cloudinitTest{{
@@ -65,9 +69,10 @@
Password: "bletch",
CACert: []byte("CA CERT\n" + testing.CACert),
},
- Config: envConfig,
- DataDir: "/var/lib/juju",
+ Constraints: envConstraints,
+ DataDir: "/var/lib/juju",
},
+ setEnvConfig: true,
expectScripts: `
mkdir -p /var/lib/juju
mkdir -p /var/log/juju
@@ -86,12 +91,14 @@
cat >> /etc/init/juju-db\.conf << 'EOF'\\ndescription "juju state database"\\nauthor "Juju Team <juju@lists\.ubuntu\.com>"\\nstart on runlevel \[2345\]\\nstop on runlevel \[!2345\]\\nrespawn\\nnormal exit 0\\n\\nexec /opt/mongo/bin/mongod --auth --dbpath=/var/lib/juju/db --sslOnNormalPorts --sslPEMKeyFile '/var/lib/juju/server\.pem' --sslPEMKeyPassword ignored --bind_ip 0\.0\.0\.0 --port 37017 --noprealloc --smallfiles\\nEOF\\n
start juju-db
mkdir -p '/var/lib/juju/agents/bootstrap'
-echo 'datadir: /var/lib/juju\\nstateservercert:\\n[^']+stateserverkey:\\n[^']+mongoport: 37017\\napiport: 17070\\noldpassword: arble\\nstateinfo:\\n addrs:\\n - localhost:37017\\n cacert:\\n[^']+ entityname: bootstrap\\n password: ""\\noldapipassword: ""\\napiinfo:\\n addrs:\\n - localhost:17070\\n cacert:\\n[^']+ entityname: bootstrap\\n password: ""\\n' > '/var/lib/juju/agents/bootstrap/agent\.conf'
+echo 'datadir: /var/lib/juju\\nstateservercert:\\n[^']+stateserverkey:\\n[^']+mongoport: 37017\\napiport: 17070\\noldpassword: arble\\nstateinfo:\\n addrs:\\n - localhost:37017\\n cacert:\\n[^']+ tag: bootstrap\\n password: ""\\noldapipassword: ""\\napiinfo:\\n addrs:\\n - localhost:17070\\n cacert:\\n[^']+ tag: bootstrap\\n password: ""\\n' > '/var/lib/juju/agents/bootstrap/agent\.conf'
chmod 600 '/var/lib/juju/agents/bootstrap/agent\.conf'
-/var/lib/juju/tools/1\.2\.3-linux-amd64/jujud bootstrap-state --data-dir '/var/lib/juju' --env-config '[^']*' --debug
+/var/lib/juju/tools/1\.2\.3-linux-amd64/jujud bootstrap-state --data-dir '/var/lib/juju' --env-config '[^']*' --constraints 'mem=2048M' --debug
rm -rf '/var/lib/juju/agents/bootstrap'
+cat > /etc/rsyslog.d/25-juju.conf << 'EOF'\\n\\n\$ModLoad imfile\\n\\n\$InputFilePollInterval 5\\n\$InputFileName /var/log/juju/machine-0.log\\n\$InputFileTag local-juju-machine-0:\\n\$InputFileStateFile machine-0\\n\$InputRunFileMonitor\\n\\n\$ModLoad imudp\\n\$UDPServerRun 514\\n\\n# Messages received from remote rsyslog machines contain a leading space so we\\n# need to account for that.\\n\$template JujuLogFormatLocal,\"%HOSTNAME%:%msg:::drop-last-lf%\\n\"\\n\$template JujuLogFormat,\"%HOSTNAME%:%msg:2:2048:drop-last-lf%\\n\"\\n\\n:syslogtag, startswith, \"juju-\" /var/log/juju/all-machines.log;JujuLogFormat\\n:syslogtag, startswith, \"local-juju-\" /var/log/juju/all-machines.log;JujuLogFormatLocal\\n& ~\\nEOF\\n
+restart rsyslog
mkdir -p '/var/lib/juju/agents/machine-0'
-echo 'datadir: /var/lib/juju\\nstateservercert:\\n[^']+stateserverkey:\\n[^']+mongoport: 37017\\napiport: 17070\\noldpassword: arble\\nstateinfo:\\n addrs:\\n - localhost:37017\\n cacert:\\n[^']+ entityname: machine-0\\n password: ""\\noldapipassword: ""\\napiinfo:\\n addrs:\\n - localhost:17070\\n cacert:\\n[^']+ entityname: machine-0\\n password: ""\\n' > '/var/lib/juju/agents/machine-0/agent\.conf'
+echo 'datadir: /var/lib/juju\\nstateservercert:\\n[^']+stateserverkey:\\n[^']+mongoport: 37017\\napiport: 17070\\noldpassword: arble\\nstateinfo:\\n addrs:\\n - localhost:37017\\n cacert:\\n[^']+ tag: machine-0\\n password: ""\\noldapipassword: ""\\napiinfo:\\n addrs:\\n - localhost:17070\\n cacert:\\n[^']+ tag: machine-0\\n password: ""\\n' > '/var/lib/juju/agents/machine-0/agent\.conf'
chmod 600 '/var/lib/juju/agents/machine-0/agent\.conf'
ln -s 1\.2\.3-linux-amd64 '/var/lib/juju/tools/machine-0'
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\\nexec /var/lib/juju/tools/machine-0/jujud machine --log-file /var/log/juju/machine-0\.log --data-dir '/var/lib/juju' --machine-id 0 --debug >> /var/log/juju/machine-0\.log 2>&1\\nEOF\\n
@@ -106,16 +113,16 @@
StateServer: false,
Tools: newSimpleTools("1.2.3-linux-amd64"),
StateInfo: &state.Info{
- Addrs: []string{"state-addr.example.com:12345"},
- EntityName: "machine-99",
- Password: "arble",
- CACert: []byte("CA CERT\n" + testing.CACert),
+ Addrs: []string{"state-addr.example.com:12345"},
+ Tag: "machine-99",
+ Password: "arble",
+ CACert: []byte("CA CERT\n" + testing.CACert),
},
APIInfo: &api.Info{
- Addrs: []string{"state-addr.example.com:54321"},
- EntityName: "machine-99",
- Password: "bletch",
- CACert: []byte("CA CERT\n" + testing.CACert),
+ Addrs: []string{"state-addr.example.com:54321"},
+ Tag: "machine-99",
+ Password: "bletch",
+ CACert: []byte("CA CERT\n" + testing.CACert),
},
},
expectScripts: `
@@ -125,8 +132,10 @@
mkdir -p \$bin
wget --no-verbose -O - 'http://foo\.com/tools/juju1\.2\.3-linux-amd64\.tgz' \| tar xz -C \$bin
echo -n 'http://foo\.com/tools/juju1\.2\.3-linux-amd64\.tgz' > \$bin/downloaded-url\.txt
+cat > /etc/rsyslog.d/25-juju.conf << 'EOF'\\n\\n\$ModLoad imfile\\n\\n\$InputFilePollInterval 5\\n\$InputFileName /var/log/juju/machine-99.log\\n\$InputFileTag juju-machine-99:\\n\$InputFileStateFile machine-99\\n\$InputRunFileMonitor\\n\\n:syslogtag, startswith, \"juju-\" @state-addr.example.com:514\\n& ~\\nEOF\\n
+restart rsyslog
mkdir -p '/var/lib/juju/agents/machine-99'
-echo 'datadir: /var/lib/juju\\noldpassword: arble\\nstateinfo:\\n addrs:\\n - state-addr\.example\.com:12345\\n cacert:\\n[^']+ entityname: machine-99\\n password: ""\\noldapipassword: ""\\napiinfo:\\n addrs:\\n - state-addr\.example\.com:54321\\n cacert:\\n[^']+ entityname: machine-99\\n password: ""\\n' > '/var/lib/juju/agents/machine-99/agent\.conf'
+echo 'datadir: /var/lib/juju\\noldpassword: arble\\nstateinfo:\\n addrs:\\n - state-addr\.example\.com:12345\\n cacert:\\n[^']+ tag: machine-99\\n password: ""\\noldapipassword: ""\\napiinfo:\\n addrs:\\n - state-addr\.example\.com:54321\\n cacert:\\n[^']+ tag: machine-99\\n password: ""\\n' > '/var/lib/juju/agents/machine-99/agent\.conf'
chmod 600 '/var/lib/juju/agents/machine-99/agent\.conf'
ln -s 1\.2\.3-linux-amd64 '/var/lib/juju/tools/machine-99'
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\\nexec /var/lib/juju/tools/machine-99/jujud machine --log-file /var/log/juju/machine-99\.log --data-dir '/var/lib/juju' --machine-id 99 --debug >> /var/log/juju/machine-99\.log 2>&1\\nEOF\\n
@@ -142,32 +151,6 @@
}
}
-func (t *cloudinitTest) check(c *C) {
- ci, err := cloudinit.New(&t.cfg)
- c.Assert(err, IsNil)
- c.Check(ci, NotNil)
- // render the cloudinit config to bytes, and then
- // back to a map so we can introspect it without
- // worrying about internal details of the cloudinit
- // package.
- data, err := ci.Render()
- c.Assert(err, IsNil)
-
- x := make(map[interface{}]interface{})
- err = goyaml.Unmarshal(data, &x)
- c.Assert(err, IsNil)
-
- c.Check(x["apt_upgrade"], Equals, true)
- c.Check(x["apt_update"], Equals, true)
-
- scripts := getScripts(x)
- scriptDiff(c, scripts, t.expectScripts)
- if t.cfg.Config != nil {
- checkEnvConfig(c, t.cfg.Config, x, scripts)
- }
- checkPackage(c, x, "git", true)
-}
-
// check that any --env-config $base64 is valid and matches t.cfg.Config
func checkEnvConfig(c *C, cfg *config.Config, x map[interface{}]interface{}, scripts []string) {
re := regexp.MustCompile(`--env-config '([\w,=]+)'`)
@@ -193,11 +176,32 @@
func (*cloudinitSuite) TestCloudInit(c *C) {
for i, test := range cloudinitTests {
c.Logf("test %d", i)
+ if test.setEnvConfig {
+ test.cfg.Config = minimalConfig(c)
+ }
ci, err := cloudinit.New(&test.cfg)
c.Assert(err, IsNil)
c.Check(ci, NotNil)
-
- test.check(c)
+ // render the cloudinit config to bytes, and then
+ // back to a map so we can introspect it without
+ // worrying about internal details of the cloudinit
+ // package.
+ data, err := ci.Render()
+ c.Assert(err, IsNil)
+
+ x := make(map[interface{}]interface{})
+ err = goyaml.Unmarshal(data, &x)
+ c.Assert(err, IsNil)
+
+ c.Check(x["apt_upgrade"], Equals, true)
+ c.Check(x["apt_update"], Equals, true)
+
+ scripts := getScripts(x)
+ scriptDiff(c, scripts, test.expectScripts)
+ if test.cfg.Config != nil {
+ checkEnvConfig(c, test.cfg.Config, x, scripts)
+ }
+ checkPackage(c, x, "git", true)
}
}
@@ -310,25 +314,25 @@
{"missing state hosts", func(cfg *cloudinit.MachineConfig) {
cfg.StateServer = false
cfg.StateInfo = &state.Info{
- EntityName: "machine-99",
- CACert: []byte(testing.CACert),
+ Tag: "machine-99",
+ CACert: []byte(testing.CACert),
}
cfg.APIInfo = &api.Info{
- Addrs: []string{"foo:35"},
- EntityName: "machine-99",
- CACert: []byte(testing.CACert),
+ Addrs: []string{"foo:35"},
+ Tag: "machine-99",
+ CACert: []byte(testing.CACert),
}
}},
{"missing API hosts", func(cfg *cloudinit.MachineConfig) {
cfg.StateServer = false
cfg.StateInfo = &state.Info{
- Addrs: []string{"foo:35"},
- EntityName: "machine-99",
- CACert: []byte(testing.CACert),
+ Addrs: []string{"foo:35"},
+ Tag: "machine-99",
+ CACert: []byte(testing.CACert),
}
cfg.APIInfo = &api.Info{
- EntityName: "machine-99",
- CACert: []byte(testing.CACert),
+ Tag: "machine-99",
+ CACert: []byte(testing.CACert),
}
}},
{"missing CA certificate", func(cfg *cloudinit.MachineConfig) {
@@ -337,8 +341,8 @@
{"missing CA certificate", func(cfg *cloudinit.MachineConfig) {
cfg.StateServer = false
cfg.StateInfo = &state.Info{
- EntityName: "machine-99",
- Addrs: []string{"host:98765"},
+ Tag: "machine-99",
+ Addrs: []string{"host:98765"},
}
}},
{"missing state server certificate", func(cfg *cloudinit.MachineConfig) {
@@ -356,38 +360,38 @@
{"missing tools URL", func(cfg *cloudinit.MachineConfig) {
cfg.Tools = &state.Tools{}
}},
- {"entity name must match started machine", func(cfg *cloudinit.MachineConfig) {
- cfg.StateServer = false
- info := *cfg.StateInfo
- info.EntityName = "machine-0"
- cfg.StateInfo = &info
- }},
- {"entity name must match started machine", func(cfg *cloudinit.MachineConfig) {
- cfg.StateServer = false
- info := *cfg.StateInfo
- info.EntityName = ""
- cfg.StateInfo = &info
- }},
- {"entity name must match started machine", func(cfg *cloudinit.MachineConfig) {
- cfg.StateServer = false
- info := *cfg.APIInfo
- info.EntityName = "machine-0"
- cfg.APIInfo = &info
- }},
- {"entity name must match started machine", func(cfg *cloudinit.MachineConfig) {
- cfg.StateServer = false
- info := *cfg.APIInfo
- info.EntityName = ""
- cfg.APIInfo = &info
- }},
- {"entity name must be blank when starting a state server", func(cfg *cloudinit.MachineConfig) {
- info := *cfg.StateInfo
- info.EntityName = "machine-0"
- cfg.StateInfo = &info
- }},
- {"entity name must be blank when starting a state server", func(cfg *cloudinit.MachineConfig) {
- info := *cfg.APIInfo
- info.EntityName = "machine-0"
+ {"entity tag must match started machine", func(cfg *cloudinit.MachineConfig) {
+ cfg.StateServer = false
+ info := *cfg.StateInfo
+ info.Tag = "machine-0"
+ cfg.StateInfo = &info
+ }},
+ {"entity tag must match started machine", func(cfg *cloudinit.MachineConfig) {
+ cfg.StateServer = false
+ info := *cfg.StateInfo
+ info.Tag = ""
+ cfg.StateInfo = &info
+ }},
+ {"entity tag must match started machine", func(cfg *cloudinit.MachineConfig) {
+ cfg.StateServer = false
+ info := *cfg.APIInfo
+ info.Tag = "machine-0"
+ cfg.APIInfo = &info
+ }},
+ {"entity tag must match started machine", func(cfg *cloudinit.MachineConfig) {
+ cfg.StateServer = false
+ info := *cfg.APIInfo
+ info.Tag = ""
+ cfg.APIInfo = &info
+ }},
+ {"entity tag must be blank when starting a state server", func(cfg *cloudinit.MachineConfig) {
+ info := *cfg.StateInfo
+ info.Tag = "machine-0"
+ cfg.StateInfo = &info
+ }},
+ {"entity tag must be blank when starting a state server", func(cfg *cloudinit.MachineConfig) {
+ info := *cfg.APIInfo
+ info.Tag = "machine-0"
cfg.APIInfo = &info
}},
{"missing mongo port", func(cfg *cloudinit.MachineConfig) {
@@ -418,7 +422,7 @@
Addrs: []string{"host:9999"},
CACert: []byte(testing.CACert),
},
- Config: envConfig,
+ Config: minimalConfig(c),
DataDir: "/var/lib/juju",
}
// check that the base configuration does not give an error
=== modified file 'environs/config.go'
--- environs/config.go 2013-02-04 01:20:55 +0000
+++ environs/config.go 2013-04-08 08:09:22 +0000
@@ -1,7 +1,6 @@
package environs
import (
- "errors"
"fmt"
"io/ioutil"
"launchpad.net/goyaml"
@@ -116,15 +115,11 @@
return &Environs{raw.Default, environs}, nil
}
-func environsPath(path string) (string, error) {
+func environsPath(path string) string {
if path == "" {
- home := os.Getenv("HOME")
- if home == "" {
- return "", errors.New("$HOME not set")
- }
- path = filepath.Join(home, ".juju/environments.yaml")
+ path = config.JujuHomePath("environments.yaml")
}
- return path, nil
+ return path
}
// ReadEnvirons reads the juju environments.yaml file
@@ -132,10 +127,7 @@
// on the file's contents.
// If path is empty, $HOME/.juju/environments.yaml is used.
func ReadEnvirons(path string) (*Environs, error) {
- environsFilepath, err := environsPath(path)
- if err != nil {
- return nil, err
- }
+ environsFilepath := environsPath(path)
data, err := ioutil.ReadFile(environsFilepath)
if err != nil {
return nil, err
@@ -149,8 +141,8 @@
// WriteEnvirons creates a new juju environments.yaml file with the specified contents.
func WriteEnvirons(path string, fileContents string) (string, error) {
- environsFilepath, err := environsPath(path)
- if err != nil {
+ environsFilepath := environsPath(path)
+ if err := os.MkdirAll(filepath.Dir(environsFilepath), 0755); err != nil {
return "", err
}
if err := ioutil.WriteFile(environsFilepath, []byte(fileContents), 0666); err != nil {
=== modified file 'environs/config/config.go'
--- environs/config/config.go 2013-02-25 17:25:18 +0000
+++ environs/config/config.go 2013-04-08 08:09:22 +0000
@@ -25,6 +25,9 @@
// When ports are opened for one machine, all machines will have the same
// port opened.
FwGlobal FirewallMode = "global"
+
+ // DefaultSeries returns the most recent Ubuntu LTS release name.
+ DefaultSeries string = "precise"
)
// Config holds an immutable environment configuration.
@@ -71,7 +74,7 @@
}
if c.m["default-series"].(string) == "" {
- c.m["default-series"] = version.Current.Series
+ c.m["default-series"] = DefaultSeries
}
// Load authorized-keys-path into authorized-keys if necessary.
@@ -112,7 +115,7 @@
return nil, fmt.Errorf("invalid agent version in environment configuration: %q", v)
}
} else {
- c.m["agent-version"] = version.Current.Number.String()
+ c.m["agent-version"] = version.CurrentNumber().String()
}
// Check firewall mode.
@@ -161,7 +164,7 @@
}
path = expandTilde(path)
if !filepath.IsAbs(path) {
- path = filepath.Join(os.Getenv("HOME"), ".juju", path)
+ path = JujuHomePath(path)
}
data, err := ioutil.ReadFile(path)
if err != nil {
@@ -294,7 +297,7 @@
}
var defaults = schema.Defaults{
- "default-series": version.Current.Series,
+ "default-series": DefaultSeries,
"authorized-keys": "",
"authorized-keys-path": "",
"firewall-mode": FwDefault,
=== modified file 'environs/config/config_test.go'
--- environs/config/config_test.go 2013-02-24 22:33:37 +0000
+++ environs/config/config_test.go 2013-04-08 08:09:22 +0000
@@ -441,7 +441,7 @@
c.Assert(err, IsNil)
c.Assert(cfg.AgentVersion(), Equals, vers)
} else {
- c.Assert(cfg.AgentVersion(), Equals, version.Current.Number)
+ c.Assert(cfg.AgentVersion(), Equals, version.CurrentNumber())
}
dev, _ := test.attrs["development"].(bool)
@@ -450,7 +450,7 @@
if series, _ := test.attrs["default-series"].(string); series != "" {
c.Assert(cfg.DefaultSeries(), Equals, series)
} else {
- c.Assert(cfg.DefaultSeries(), Equals, version.Current.Series)
+ c.Assert(cfg.DefaultSeries(), Equals, config.DefaultSeries)
}
if m, _ := test.attrs["firewall-mode"].(string); m != "" {
@@ -519,7 +519,6 @@
"name": "my-name",
"authorized-keys": "my-keys",
"firewall-mode": string(config.FwDefault),
- "default-series": version.Current.Series,
"admin-secret": "foo",
"unknown": "my-unknown",
"ca-private-key": "",
@@ -531,7 +530,8 @@
// These attributes are added if not set.
attrs["development"] = false
- attrs["agent-version"] = version.Current.Number.String()
+ attrs["agent-version"] = version.CurrentNumber().String()
+ attrs["default-series"] = config.DefaultSeries
c.Assert(cfg.AllAttrs(), DeepEquals, attrs)
c.Assert(cfg.UnknownAttrs(), DeepEquals, map[string]interface{}{"unknown": "my-unknown"})
@@ -546,8 +546,9 @@
}
type fakeHome struct {
- oldHome string
- files []testFile
+ oldHome string
+ oldJujuHome string
+ files []testFile
}
func makeFakeHome(c *C, files []testFile) fakeHome {
@@ -561,10 +562,12 @@
c.Assert(err, IsNil)
}
os.Setenv("HOME", homeDir)
- return fakeHome{oldHome, files}
+ oldJujuHome := config.SetJujuHome(filepath.Join(homeDir, ".juju"))
+ return fakeHome{oldHome, oldJujuHome, files}
}
func (h fakeHome) restore() {
+ config.SetJujuHome(h.oldJujuHome)
os.Setenv("HOME", h.oldHome)
}
=== added file 'environs/config/home.go'
--- environs/config/home.go 1970-01-01 00:00:00 +0000
+++ environs/config/home.go 2013-04-08 08:09:22 +0000
@@ -0,0 +1,41 @@
+package config
+
+import (
+ "path/filepath"
+ "sync"
+)
+
+// jujuHome stores the path to the juju configuration
+// folder, which is only meaningful when running the juju
+// CLI tool, and is typically defined by $JUJU_HOME or
+// $HOME/.juju as default.
+var (
+ jujuHomeMu sync.Mutex
+ jujuHome string
+)
+
+// SetJujuHome sets the value of juju home and
+// returns the current one.
+func SetJujuHome(newJujuHome string) string {
+ jujuHomeMu.Lock()
+ defer jujuHomeMu.Unlock()
+
+ oldJujuHome := jujuHome
+ jujuHome = newJujuHome
+ return oldJujuHome
+}
+
+// JujuHome returns the current juju home.
+func JujuHome() string {
+ if jujuHome == "" {
+ panic("juju home hasn't been initialized")
+ }
+ return jujuHome
+}
+
+// JujuHomePath returns the path to a file in the
+// current juju home.
+func JujuHomePath(names ...string) string {
+ all := append([]string{JujuHome()}, names...)
+ return filepath.Join(all...)
+}
=== added file 'environs/config/home_test.go'
--- environs/config/home_test.go 1970-01-01 00:00:00 +0000
+++ environs/config/home_test.go 2013-04-08 08:09:22 +0000
@@ -0,0 +1,35 @@
+package config_test
+
+import (
+ . "launchpad.net/gocheck"
+ "path/filepath"
+
+ "launchpad.net/juju-core/environs/config"
+)
+
+type JujuHomeSuite struct {
+ jujuHome string
+}
+
+var _ = Suite(&JujuHomeSuite{})
+
+func (s *JujuHomeSuite) TestStandardHome(c *C) {
+ testJujuHome := c.MkDir()
+ defer config.SetJujuHome(config.SetJujuHome(testJujuHome))
+ c.Assert(config.JujuHome(), Equals, testJujuHome)
+}
+
+func (s *JujuHomeSuite) TestErrorHome(c *C) {
+ // Invalid juju home leads to panic when retrieving.
+ f := func() { _ = config.JujuHome() }
+ c.Assert(f, PanicMatches, "juju home hasn't been initialized")
+ f = func() { _ = config.JujuHomePath("environments.yaml") }
+ c.Assert(f, PanicMatches, "juju home hasn't been initialized")
+}
+
+func (s *JujuHomeSuite) TestHomePath(c *C) {
+ testJujuHome := c.MkDir()
+ defer config.SetJujuHome(config.SetJujuHome(testJujuHome))
+ envPath := config.JujuHomePath("environments.yaml")
+ c.Assert(envPath, Equals, filepath.Join(testJujuHome, "environments.yaml"))
+}
=== modified file 'environs/config_test.go'
--- environs/config_test.go 2013-02-04 01:20:55 +0000
+++ environs/config_test.go 2013-04-08 08:09:22 +0000
@@ -1,7 +1,6 @@
package environs_test
import (
- "io/ioutil"
. "launchpad.net/gocheck"
"launchpad.net/juju-core/environs"
"launchpad.net/juju-core/environs/config"
@@ -9,7 +8,6 @@
"launchpad.net/juju-core/state"
"launchpad.net/juju-core/testing"
"launchpad.net/juju-core/version"
- "os"
"path/filepath"
)
@@ -76,7 +74,7 @@
}
func (suite) TestInvalidEnv(c *C) {
- defer makeFakeHome(c, "only").restore()
+ defer testing.MakeFakeHomeNoEnvironments(c, "only").Restore()
for i, t := range invalidEnvTests {
c.Logf("running test %v", i)
es, err := environs.ReadEnvironsBytes([]byte(t.env))
@@ -132,7 +130,7 @@
}
func (suite) TestConfig(c *C) {
- defer makeFakeHome(c, "only", "valid", "one", "two").restore()
+ defer testing.MakeFakeHomeNoEnvironments(c, "only", "valid", "one", "two").Restore()
for i, t := range configTests {
c.Logf("running test %v", i)
es, err := environs.ReadEnvironsBytes([]byte(t.env))
@@ -142,7 +140,7 @@
}
func (suite) TestDefaultConfigFile(c *C) {
- defer makeFakeHome(c, "only").restore()
+ defer testing.MakeEmptyFakeHome(c).Restore()
env := `
environments:
@@ -153,7 +151,7 @@
`
outfile, err := environs.WriteEnvirons("", env)
c.Assert(err, IsNil)
- path := homePath(".juju", "environments.yaml")
+ path := testing.HomePath(".juju", "environments.yaml")
c.Assert(path, Equals, outfile)
es, err := environs.ReadEnvirons("")
@@ -164,7 +162,7 @@
}
func (suite) TestNamedConfigFile(c *C) {
- defer makeFakeHome(c, "only").restore()
+ defer testing.MakeFakeHomeNoEnvironments(c, "only").Restore()
env := `
environments:
@@ -185,22 +183,6 @@
c.Assert(e.Name(), Equals, "only")
}
-func (suite) TestWriteConfigNoHome(c *C) {
- defer makeFakeHome(c).restore()
- os.Setenv("HOME", "")
-
- env := `
-environments:
- only:
- type: dummy
- state-server: false
- authorized-keys: i-am-a-key
-`
- _, err := environs.WriteEnvirons("", env)
- c.Assert(err, NotNil)
- c.Assert(err.Error(), Equals, "$HOME not set")
-}
-
func (suite) TestConfigRoundTrip(c *C) {
cfg, err := config.New(map[string]interface{}{
"name": "bladaam",
@@ -221,7 +203,7 @@
}
func (suite) TestBootstrapConfig(c *C) {
- defer makeFakeHome(c, "bladaam").restore()
+ defer testing.MakeFakeHomeNoEnvironments(c, "bladaam").Restore()
cfg, err := config.New(map[string]interface{}{
"name": "bladaam",
"type": "dummy",
@@ -250,35 +232,3 @@
expect["agent-version"] = "1.2.3"
c.Assert(cfg1.AllAttrs(), DeepEquals, expect)
}
-
-type fakeHome string
-
-func makeFakeHome(c *C, certNames ...string) fakeHome {
- oldHome := os.Getenv("HOME")
- os.Setenv("HOME", c.MkDir())
-
- err := os.Mkdir(homePath(".juju"), 0777)
- c.Assert(err, IsNil)
- for _, name := range certNames {
- err := ioutil.WriteFile(homePath(".juju", name+"-cert.pem"), []byte(testing.CACert), 0666)
- c.Assert(err, IsNil)
- err = ioutil.WriteFile(homePath(".juju", name+"-private-key.pem"), []byte(testing.CAKey), 0666)
- c.Assert(err, IsNil)
- }
-
- err = os.Mkdir(homePath(".ssh"), 0777)
- c.Assert(err, IsNil)
- err = ioutil.WriteFile(homePath(".ssh", "id_rsa.pub"), []byte("auth key\n"), 0666)
- c.Assert(err, IsNil)
-
- return fakeHome(oldHome)
-}
-
-func homePath(names ...string) string {
- all := append([]string{os.Getenv("HOME")}, names...)
- return filepath.Join(all...)
-}
-
-func (h fakeHome) restore() {
- os.Setenv("HOME", string(h))
-}
=== modified file 'environs/dummy/environs.go'
--- environs/dummy/environs.go 2013-02-27 13:28:43 +0000
+++ environs/dummy/environs.go 2013-04-08 08:09:22 +0000
@@ -22,12 +22,15 @@
import (
"errors"
"fmt"
+ "launchpad.net/juju-core/constraints"
"launchpad.net/juju-core/environs"
"launchpad.net/juju-core/environs/config"
"launchpad.net/juju-core/log"
"launchpad.net/juju-core/schema"
"launchpad.net/juju-core/state"
"launchpad.net/juju-core/state/api"
+ "launchpad.net/juju-core/state/api/params"
+ "launchpad.net/juju-core/state/apiserver"
"launchpad.net/juju-core/testing"
"launchpad.net/juju-core/trivial"
"launchpad.net/juju-core/version"
@@ -58,17 +61,21 @@
Env string
}
-type OpBootstrap GenericOperation
+type OpBootstrap struct {
+ Env string
+ Constraints constraints.Value
+}
type OpDestroy GenericOperation
type OpStartInstance struct {
- Env string
- MachineId string
- Instance environs.Instance
- Info *state.Info
- APIInfo *api.Info
- Secret string
+ Env string
+ MachineId string
+ Instance environs.Instance
+ Constraints constraints.Value
+ Info *state.Info
+ APIInfo *api.Info
+ Secret string
}
type OpStopInstances struct {
@@ -80,14 +87,14 @@
Env string
MachineId string
InstanceId state.InstanceId
- Ports []state.Port
+ Ports []params.Port
}
type OpClosePorts struct {
Env string
MachineId string
InstanceId state.InstanceId
- Ports []state.Port
+ Ports []params.Port
}
type OpPutFile GenericOperation
@@ -112,14 +119,14 @@
mu sync.Mutex
maxId int // maximum instance id allocated so far.
insts map[state.InstanceId]*instance
- globalPorts map[state.Port]bool
+ globalPorts map[params.Port]bool
firewallMode config.FirewallMode
bootstrapped bool
storageDelay time.Duration
storage *storage
publicStorage *storage
httpListener net.Listener
- apiServer *api.Server
+ apiServer *apiserver.Server
apiState *state.State
}
@@ -165,7 +172,7 @@
// operation listener. All opened environments after Reset will share
// the same underlying state.
func Reset() {
- log.Printf("environs/dummy: reset environment")
+ log.Infof("environs/dummy: reset environment")
p := &providerInstance
p.mu.Lock()
defer p.mu.Unlock()
@@ -214,7 +221,7 @@
name: name,
ops: ops,
insts: make(map[state.InstanceId]*instance),
- globalPorts: make(map[state.Port]bool),
+ globalPorts: make(map[params.Port]bool),
firewallMode: fwmode,
}
s.storage = newStorage(s, "/"+name+"/private")
@@ -228,13 +235,22 @@
// that looks like a tools archive so Bootstrap can
// find some tools and initialise the state correctly.
func putFakeTools(s environs.StorageWriter) {
- log.Printf("environs/dummy: putting fake tools")
- path := environs.ToolsStoragePath(version.Current)
+ log.Infof("environs/dummy: putting fake tools")
+ toolsVersion := version.Current
+ path := environs.ToolsStoragePath(toolsVersion)
toolsContents := "tools archive, honest guv"
err := s.Put(path, strings.NewReader(toolsContents), int64(len(toolsContents)))
if err != nil {
panic(err)
}
+ if toolsVersion.Series != config.DefaultSeries {
+ toolsVersion.Series = config.DefaultSeries
+ path = environs.ToolsStoragePath(toolsVersion)
+ err = s.Put(path, strings.NewReader(toolsContents), int64(len(toolsContents)))
+ if err != nil {
+ panic(err)
+ }
+ }
}
// listen starts a network listener listening for http
@@ -424,7 +440,7 @@
return e.state.name
}
-func (e *environ) Bootstrap(uploadTools bool, cert, key []byte) error {
+func (e *environ) Bootstrap(cons constraints.Value, cert, key []byte) error {
defer delay()
if err := e.checkBroken("Bootstrap"); err != nil {
return err
@@ -438,21 +454,16 @@
}
var tools *state.Tools
var err error
- if uploadTools {
- tools, err = environs.PutTools(e.Storage(), nil)
- if err != nil {
- return err
- }
- } else {
- flags := environs.HighestVersion | environs.CompatVersion
- tools, err = environs.FindTools(e, version.Current, flags)
- if err != nil {
- return err
- }
+
+ flags := environs.CompatVersion
+ tools, err = environs.FindTools(e, version.Current, flags)
+ if err != nil {
+ return err
}
+
e.state.mu.Lock()
defer e.state.mu.Unlock()
- e.state.ops <- OpBootstrap{Env: e.state.name}
+ e.state.ops <- OpBootstrap{Env: e.state.name, Constraints: cons}
if e.state.bootstrapped {
return fmt.Errorf("environment is already bootstrapped")
}
@@ -462,10 +473,13 @@
if err != nil {
return fmt.Errorf("cannot make bootstrap config: %v", err)
}
- st, err := state.Initialize(info, cfg)
+ st, err := state.Initialize(info, cfg, state.DefaultDialOpts())
if err != nil {
panic(err)
}
+ if err := st.SetEnvironConstraints(cons); err != nil {
+ panic(err)
+ }
if err := st.SetAdminMongoPassword(trivial.PasswordHash(password)); err != nil {
panic(err)
}
@@ -475,7 +489,7 @@
if err != nil {
panic(err)
}
- e.state.apiServer, err = api.NewServer(st, "localhost:0", []byte(testing.ServerCert), []byte(testing.ServerKey))
+ e.state.apiServer, err = apiserver.NewServer(st, "localhost:0", []byte(testing.ServerCert), []byte(testing.ServerKey))
if err != nil {
panic(err)
}
@@ -504,7 +518,10 @@
}
func (e *environ) AssignmentPolicy() state.AssignmentPolicy {
- return state.AssignUnused
+ // Although dummy does not actually start instances, it must respect the
+ // conservative assignment policy for the providers that do instantiate
+ // machines.
+ return state.AssignNew
}
func (e *environ) Config() *config.Config {
@@ -538,9 +555,9 @@
return nil
}
-func (e *environ) StartInstance(machineId string, info *state.Info, apiInfo *api.Info, tools *state.Tools) (environs.Instance, error) {
+func (e *environ) StartInstance(machineId string, series string, cons constraints.Value, info *state.Info, apiInfo *api.Info) (environs.Instance, error) {
defer delay()
- log.Printf("environs/dummy: dummy startinstance, machine %s", machineId)
+ log.Infof("environs/dummy: dummy startinstance, machine %s", machineId)
if err := e.checkBroken("StartInstance"); err != nil {
return nil, err
}
@@ -549,30 +566,32 @@
if _, ok := e.Config().CACert(); !ok {
return nil, fmt.Errorf("no CA certificate in environment configuration")
}
- if info.EntityName != state.MachineEntityName(machineId) {
- return nil, fmt.Errorf("entity name must match started machine")
- }
- if apiInfo.EntityName != state.MachineEntityName(machineId) {
- return nil, fmt.Errorf("entity name must match started machine")
- }
- if tools != nil && (strings.HasPrefix(tools.Series, "unknown") || strings.HasPrefix(tools.Arch, "unknown")) {
- return nil, fmt.Errorf("cannot find image for %s-%s", tools.Series, tools.Arch)
+ if info.Tag != state.MachineTag(machineId) {
+ return nil, fmt.Errorf("entity tag must match started machine")
+ }
+ if apiInfo.Tag != state.MachineTag(machineId) {
+ return nil, fmt.Errorf("entity tag must match started machine")
+ }
+ if strings.HasPrefix(series, "unknown") {
+ return nil, &environs.NotFoundError{fmt.Errorf("no compatible tools found")}
}
i := &instance{
state: e.state,
id: state.InstanceId(fmt.Sprintf("%s-%d", e.state.name, e.state.maxId)),
- ports: make(map[state.Port]bool),
+ ports: make(map[params.Port]bool),
machineId: machineId,
+ series: series,
}
e.state.insts[i.id] = i
e.state.maxId++
e.state.ops <- OpStartInstance{
- Env: e.state.name,
- MachineId: machineId,
- Instance: i,
- Info: info,
- APIInfo: apiInfo,
- Secret: e.ecfg().secret(),
+ Env: e.state.name,
+ MachineId: machineId,
+ Constraints: cons,
+ Instance: i,
+ Info: info,
+ APIInfo: apiInfo,
+ Secret: e.ecfg().secret(),
}
return i, nil
}
@@ -633,7 +652,7 @@
return insts, nil
}
-func (e *environ) OpenPorts(ports []state.Port) error {
+func (e *environ) OpenPorts(ports []params.Port) error {
e.state.mu.Lock()
defer e.state.mu.Unlock()
if e.state.firewallMode != config.FwGlobal {
@@ -646,7 +665,7 @@
return nil
}
-func (e *environ) ClosePorts(ports []state.Port) error {
+func (e *environ) ClosePorts(ports []params.Port) error {
e.state.mu.Lock()
defer e.state.mu.Unlock()
if e.state.firewallMode != config.FwGlobal {
@@ -659,7 +678,7 @@
return nil
}
-func (e *environ) Ports() (ports []state.Port, err error) {
+func (e *environ) Ports() (ports []params.Port, err error) {
e.state.mu.Lock()
defer e.state.mu.Unlock()
if e.state.firewallMode != config.FwGlobal {
@@ -679,9 +698,10 @@
type instance struct {
state *environState
- ports map[state.Port]bool
+ ports map[params.Port]bool
id state.InstanceId
machineId string
+ series string
}
func (inst *instance) Id() state.InstanceId {
@@ -697,9 +717,9 @@
return inst.DNSName()
}
-func (inst *instance) OpenPorts(machineId string, ports []state.Port) error {
+func (inst *instance) OpenPorts(machineId string, ports []params.Port) error {
defer delay()
- log.Printf("environs/dummy: openPorts %s, %#v", machineId, ports)
+ log.Infof("environs/dummy: openPorts %s, %#v", machineId, ports)
if inst.state.firewallMode != config.FwInstance {
return fmt.Errorf("invalid firewall mode for opening ports on instance: %q",
inst.state.firewallMode)
@@ -721,7 +741,7 @@
return nil
}
-func (inst *instance) ClosePorts(machineId string, ports []state.Port) error {
+func (inst *instance) ClosePorts(machineId string, ports []params.Port) error {
defer delay()
if inst.state.firewallMode != config.FwInstance {
return fmt.Errorf("invalid firewall mode for closing ports on instance: %q",
@@ -744,7 +764,7 @@
return nil
}
-func (inst *instance) Ports(machineId string) (ports []state.Port, err error) {
+func (inst *instance) Ports(machineId string) (ports []params.Port, err error) {
defer delay()
if inst.state.firewallMode != config.FwInstance {
return nil, fmt.Errorf("invalid firewall mode for retrieving ports from instance: %q",
@@ -770,7 +790,7 @@
// pause execution to simulate the latency of a real provider
func delay() {
if providerDelay > 0 {
- log.Printf("environs/dummy: pausing for %v", providerDelay)
+ log.Infof("environs/dummy: pausing for %v", providerDelay)
<-time.After(providerDelay)
}
}
=== modified file 'environs/dummy/environs_test.go'
--- environs/dummy/environs_test.go 2012-11-27 09:19:59 +0000
+++ environs/dummy/environs_test.go 2013-04-08 08:09:22 +0000
@@ -20,12 +20,12 @@
"ca-private-key": testing.CAKey,
}
Suite(&jujutest.LiveTests{
- Config: attrs,
+ TestConfig: jujutest.TestConfig{attrs},
CanOpenState: true,
HasProvisioner: false,
})
Suite(&jujutest.Tests{
- Config: attrs,
+ TestConfig: jujutest.TestConfig{attrs},
})
}
=== modified file 'environs/ec2/ec2.go'
--- environs/ec2/ec2.go 2013-02-27 13:28:43 +0000
+++ environs/ec2/ec2.go 2013-04-08 08:09:22 +0000
@@ -6,12 +6,14 @@
"launchpad.net/goamz/aws"
"launchpad.net/goamz/ec2"
"launchpad.net/goamz/s3"
+ "launchpad.net/juju-core/constraints"
"launchpad.net/juju-core/environs"
"launchpad.net/juju-core/environs/cloudinit"
"launchpad.net/juju-core/environs/config"
"launchpad.net/juju-core/log"
"launchpad.net/juju-core/state"
"launchpad.net/juju-core/state/api"
+ "launchpad.net/juju-core/state/api/params"
"launchpad.net/juju-core/trivial"
"launchpad.net/juju-core/version"
"net/http"
@@ -127,7 +129,7 @@
}
func (p environProvider) Open(cfg *config.Config) (environs.Environ, error) {
- log.Printf("environs/ec2: opening environment %q", cfg.Name())
+ log.Infof("environs/ec2: opening environment %q", cfg.Name())
e := new(environ)
err := e.SetConfig(cfg)
if err != nil {
@@ -236,12 +238,23 @@
return e.publicStorageUnlocked
}
-func (e *environ) Bootstrap(uploadTools bool, cert, key []byte) error {
+// TODO(thumper): this code is duplicated in ec2 and openstack. Ideally we
+// should refactor the tools selection criteria with the version that is in
+// environs. The constraints work will require this refactoring.
+func findTools(env *environ) (*state.Tools, error) {
+ flags := environs.HighestVersion | environs.CompatVersion
+ v := version.Current
+ v.Series = env.Config().DefaultSeries()
+ // TODO: set Arch based on constraints (when they are landed)
+ return environs.FindTools(env, v, flags)
+}
+
+func (e *environ) Bootstrap(cons constraints.Value, cert, key []byte) error {
password := e.Config().AdminSecret()
if password == "" {
return fmt.Errorf("admin-secret is required for bootstrap")
}
- log.Printf("environs/ec2: bootstrapping environment %q", e.name)
+ log.Infof("environs/ec2: bootstrapping environment %q", e.name)
// If the state file exists, it might actually have just been
// removed by Destroy, and eventual consistency has not caught
// up yet, so we retry to verify if that is happening.
@@ -258,21 +271,12 @@
if _, notFound := err.(*environs.NotFoundError); !notFound {
return fmt.Errorf("cannot query old bootstrap state: %v", err)
}
- var tools *state.Tools
- if uploadTools {
- tools, err = environs.PutTools(e.Storage(), nil)
- if err != nil {
- return fmt.Errorf("cannot upload tools: %v", err)
- }
- } else {
- flags := environs.HighestVersion | environs.CompatVersion
- v := version.Current
- v.Series = e.Config().DefaultSeries()
- tools, err = environs.FindTools(e, v, flags)
- if err != nil {
- return fmt.Errorf("cannot find tools: %v", err)
- }
+
+ tools, err := findTools(e)
+ if err != nil {
+ return fmt.Errorf("cannot find tools: %v", err)
}
+
config, err := environs.BootstrapConfig(providerInstance, e.Config(), tools)
if err != nil {
return fmt.Errorf("unable to determine inital configuration: %v", err)
@@ -281,12 +285,11 @@
if !hasCert {
return fmt.Errorf("no CA certificate in environment configuration")
}
- v := version.Current
- v.Series = tools.Series
- v.Arch = tools.Arch
- mongoURL := environs.MongoURL(e, v)
+ mongoURL := environs.MongoURL(e, tools.Series, tools.Arch)
inst, err := e.startInstance(&startInstanceParams{
- machineId: "0",
+ machineId: "0",
+ series: tools.Series,
+ constraints: cons,
info: &state.Info{
Password: trivial.PasswordHash(password),
CACert: caCert,
@@ -336,7 +339,7 @@
var apiAddrs []string
// Wait for the DNS names of any of the instances
// to become available.
- log.Printf("environs/ec2: waiting for DNS name(s) of state server instances %v", st.StateInstances)
+ log.Infof("environs/ec2: waiting for DNS name(s) of state server instances %v", st.StateInstances)
for a := longAttempt.Start(); len(stateAddrs) == 0 && a.Next(); {
insts, err := e.Instances(st.StateInstances)
if err != nil && err != environs.ErrPartialInstances {
@@ -368,15 +371,21 @@
// AssignmentPolicy for EC2 is to deploy units only on machines without other
// units already assigned, and to launch new machines as required.
func (e *environ) AssignmentPolicy() state.AssignmentPolicy {
- return state.AssignUnused
+ // Until we get proper containers to install units into, we shouldn't
+ // reuse dirty machines, as we cannot guarantee that when units were
+ // removed, it was left in a clean state. Once we have good
+ // containerisation for the units, we should be able to have the ability
+ // to assign back to unused machines.
+ return state.AssignNew
}
-func (e *environ) StartInstance(machineId string, info *state.Info, apiInfo *api.Info, tools *state.Tools) (environs.Instance, error) {
+func (e *environ) StartInstance(machineId string, series string, cons constraints.Value, info *state.Info, apiInfo *api.Info) (environs.Instance, error) {
return e.startInstance(&startInstanceParams{
- machineId: machineId,
- info: info,
- apiInfo: apiInfo,
- tools: tools,
+ machineId: machineId,
+ series: series,
+ constraints: cons,
+ info: info,
+ apiInfo: apiInfo,
})
}
@@ -395,6 +404,7 @@
MachineId: scfg.machineId,
AuthorizedKeys: e.ecfg().AuthorizedKeys(),
Config: scfg.config,
+ Constraints: scfg.constraints,
}
cloudcfg, err := cloudinit.New(cfg)
if err != nil {
@@ -411,6 +421,8 @@
type startInstanceParams struct {
machineId string
+ series string
+ constraints constraints.Value
info *state.Info
apiInfo *api.Info
tools *state.Tools
@@ -424,24 +436,28 @@
// startInstance is the internal version of StartInstance, used by Bootstrap
// as well as via StartInstance itself.
func (e *environ) startInstance(scfg *startInstanceParams) (environs.Instance, error) {
+ // TODO(fwereade): choose tools *after* getting an instance spec; take series
+ // from scfg and available arches from list of known and compatible tools.
if scfg.tools == nil {
var err error
flags := environs.HighestVersion | environs.CompatVersion
- scfg.tools, err = environs.FindTools(e, version.Current, flags)
+ v := version.Current
+ v.Series = scfg.series
+ scfg.tools, err = environs.FindTools(e, v, flags)
if err != nil {
return nil, err
}
}
- log.Printf("environs/ec2: starting machine %s in %q running tools version %q from %q", scfg.machineId, e.name, scfg.tools.Binary, scfg.tools.URL)
+ log.Infof("environs/ec2: starting machine %s in %q running tools version %q from %q", scfg.machineId, e.name, scfg.tools.Binary, scfg.tools.URL)
spec, err := findInstanceSpec(&instanceConstraint{
- series: scfg.tools.Series,
- arch: scfg.tools.Arch,
- region: e.ecfg().region(),
+ region: e.ecfg().region(),
+ series: scfg.series,
+ arches: []string{scfg.tools.Arch},
+ constraints: scfg.constraints,
})
if err != nil {
- return nil, fmt.Errorf("cannot find image satisfying constraints: %v", err)
+ return nil, err
}
- // TODO quick sanity check that we can access the tools URL?
userData, err := e.userData(scfg)
if err != nil {
return nil, fmt.Errorf("cannot make user data: %v", err)
@@ -454,11 +470,11 @@
for a := shortAttempt.Start(); a.Next(); {
instances, err = e.ec2().RunInstances(&ec2.RunInstances{
- ImageId: spec.imageId,
+ ImageId: spec.image.id,
MinCount: 1,
MaxCount: 1,
UserData: userData,
- InstanceType: "m1.small",
+ InstanceType: spec.instanceType,
SecurityGroups: groups,
})
if err == nil || ec2ErrCode(err) != "InvalidGroup.NotFound" {
@@ -472,7 +488,7 @@
return nil, fmt.Errorf("expected 1 started instance, got %d", len(instances.Instances))
}
inst := &instance{e, &instances.Instances[0]}
- log.Printf("environs/ec2: started instance %q", inst.Id())
+ log.Infof("environs/ec2: started instance %q", inst.Id())
return inst, nil
}
@@ -578,7 +594,7 @@
}
func (e *environ) Destroy(ensureInsts []environs.Instance) error {
- log.Printf("environs/ec2: destroying environment %q", e.name)
+ log.Infof("environs/ec2: destroying environment %q", e.name)
insts, err := e.AllInstances()
if err != nil {
return fmt.Errorf("cannot get instances: %v", err)
@@ -611,7 +627,7 @@
return st.deleteAll()
}
-func portsToIPPerms(ports []state.Port) []ec2.IPPerm {
+func portsToIPPerms(ports []params.Port) []ec2.IPPerm {
ipPerms := make([]ec2.IPPerm, len(ports))
for i, p := range ports {
ipPerms[i] = ec2.IPPerm{
@@ -624,7 +640,7 @@
return ipPerms
}
-func (e *environ) openPortsInGroup(name string, ports []state.Port) error {
+func (e *environ) openPortsInGroup(name string, ports []params.Port) error {
if len(ports) == 0 {
return nil
}
@@ -654,7 +670,7 @@
return nil
}
-func (e *environ) closePortsInGroup(name string, ports []state.Port) error {
+func (e *environ) closePortsInGroup(name string, ports []params.Port) error {
if len(ports) == 0 {
return nil
}
@@ -669,7 +685,7 @@
return nil
}
-func (e *environ) portsInGroup(name string) (ports []state.Port, err error) {
+func (e *environ) portsInGroup(name string) (ports []params.Port, err error) {
g := ec2.SecurityGroup{Name: name}
resp, err := e.ec2().SecurityGroups([]ec2.SecurityGroup{g}, nil)
if err != nil {
@@ -680,11 +696,11 @@
}
for _, p := range resp.Groups[0].IPPerms {
if len(p.SourceIPs) != 1 {
- log.Printf("environs/ec2: unexpected IP permission found: %v", p)
+ log.Warningf("environs/ec2: unexpected IP permission found: %v", p)
continue
}
for i := p.FromPort; i <= p.ToPort; i++ {
- ports = append(ports, state.Port{
+ ports = append(ports, params.Port{
Protocol: p.Protocol,
Number: i,
})
@@ -694,7 +710,7 @@
return ports, nil
}
-func (e *environ) OpenPorts(ports []state.Port) error {
+func (e *environ) OpenPorts(ports []params.Port) error {
if e.Config().FirewallMode() != config.FwGlobal {
return fmt.Errorf("invalid firewall mode for opening ports on environment: %q",
e.Config().FirewallMode())
@@ -702,11 +718,11 @@
if err := e.openPortsInGroup(e.globalGroupName(), ports); err != nil {
return err
}
- log.Printf("environs/ec2: opened ports in global group: %v", ports)
+ log.Infof("environs/ec2: opened ports in global group: %v", ports)
return nil
}
-func (e *environ) ClosePorts(ports []state.Port) error {
+func (e *environ) ClosePorts(ports []params.Port) error {
if e.Config().FirewallMode() != config.FwGlobal {
return fmt.Errorf("invalid firewall mode for closing ports on environment: %q",
e.Config().FirewallMode())
@@ -714,11 +730,11 @@
if err := e.closePortsInGroup(e.globalGroupName(), ports); err != nil {
return err
}
- log.Printf("environs/ec2: closed ports in global group: %v", ports)
+ log.Infof("environs/ec2: closed ports in global group: %v", ports)
return nil
}
-func (e *environ) Ports() ([]state.Port, error) {
+func (e *environ) Ports() ([]params.Port, error) {
if e.Config().FirewallMode() != config.FwGlobal {
return nil, fmt.Errorf("invalid firewall mode for retrieving ports from environment: %q",
e.Config().FirewallMode())
@@ -777,7 +793,7 @@
return "juju-" + e.name
}
-func (inst *instance) OpenPorts(machineId string, ports []state.Port) error {
+func (inst *instance) OpenPorts(machineId string, ports []params.Port) error {
if inst.e.Config().FirewallMode() != config.FwInstance {
return fmt.Errorf("invalid firewall mode for opening ports on instance: %q",
inst.e.Config().FirewallMode())
@@ -786,11 +802,11 @@
if err := inst.e.openPortsInGroup(name, ports); err != nil {
return err
}
- log.Printf("environs/ec2: opened ports in security group %s: %v", name, ports)
+ log.Infof("environs/ec2: opened ports in security group %s: %v", name, ports)
return nil
}
-func (inst *instance) ClosePorts(machineId string, ports []state.Port) error {
+func (inst *instance) ClosePorts(machineId string, ports []params.Port) error {
if inst.e.Config().FirewallMode() != config.FwInstance {
return fmt.Errorf("invalid firewall mode for closing ports on instance: %q",
inst.e.Config().FirewallMode())
@@ -799,11 +815,11 @@
if err := inst.e.closePortsInGroup(name, ports); err != nil {
return err
}
- log.Printf("environs/ec2: closed ports in security group %s: %v", name, ports)
+ log.Infof("environs/ec2: closed ports in security group %s: %v", name, ports)
return nil
}
-func (inst *instance) Ports(machineId string) ([]state.Port, error) {
+func (inst *instance) Ports(machineId string) ([]params.Port, error) {
if inst.e.Config().FirewallMode() != config.FwInstance {
return nil, fmt.Errorf("invalid firewall mode for retrieving ports from instance: %q",
inst.e.Config().FirewallMode())
=== modified file 'environs/ec2/export_test.go'
--- environs/ec2/export_test.go 2013-01-17 05:09:54 +0000
+++ environs/ec2/export_test.go 2013-04-08 08:09:22 +0000
@@ -5,6 +5,7 @@
"launchpad.net/goamz/ec2"
"launchpad.net/goamz/s3"
"launchpad.net/juju-core/environs"
+ "launchpad.net/juju-core/environs/jujutest"
"launchpad.net/juju-core/state"
"launchpad.net/juju-core/trivial"
"net/http"
@@ -54,27 +55,47 @@
}
}
+var testRoundTripper = &jujutest.ProxyRoundTripper{}
+
+func init() {
+ // Prepare mock http transport for overriding metadata and images output in tests
+ http.DefaultTransport.(*http.Transport).RegisterProtocol("test", testRoundTripper)
+}
+
+// TODO: Apart from overriding different hardcoded hosts, these two test helpers are identical. Let's share.
+
var origImagesHost = imagesHost
-func init() {
- // Make the images data accessible through the "file" protocol.
- http.DefaultTransport.(*http.Transport).RegisterProtocol("file", http.NewFileTransport(http.Dir("testdata")))
-}
-
-func UseTestImageData(local bool) {
- if local {
- imagesHost = "file:"
+// UseTestImageData causes the given content to be served
+// when the ec2 client asks for image data.
+func UseTestImageData(content []jujutest.FileContent) {
+ if content != nil {
+ testRoundTripper.Sub = jujutest.NewVirtualRoundTripper(content)
+ imagesHost = "test:"
} else {
+ testRoundTripper.Sub = nil
imagesHost = origImagesHost
}
}
+// UseTestInstanceTypeData causes the given instance type
+// cost data to be served for the "test" region.
+func UseTestInstanceTypeData(content map[string]uint64) {
+ if content != nil {
+ allRegionCosts["test"] = content
+ } else {
+ delete(allRegionCosts, "test")
+ }
+}
+
var origMetadataHost = metadataHost
-func UseTestMetadata(local bool) {
- if local {
- metadataHost = "file:"
+func UseTestMetadata(content []jujutest.FileContent) {
+ if content != nil {
+ testRoundTripper.Sub = jujutest.NewVirtualRoundTripper(content)
+ metadataHost = "test:"
} else {
+ testRoundTripper.Sub = nil
metadataHost = origMetadataHost
}
}
=== modified file 'environs/ec2/image.go'
--- environs/ec2/image.go 2012-07-10 08:23:54 +0000
+++ environs/ec2/image.go 2013-04-08 08:09:22 +0000
@@ -3,23 +3,72 @@
import (
"bufio"
"fmt"
+ "io"
+ "launchpad.net/juju-core/constraints"
"net/http"
+ "sort"
"strings"
)
// instanceConstraint constrains the possible instances that may be
// chosen by the ec2 provider.
type instanceConstraint struct {
- series string // Ubuntu release name.
- arch string
- region string
+ region string
+ series string
+ arches []string
+ constraints constraints.Value
}
-// instanceSpec specifies a particular kind of instance.
+// instanceSpec holds an instance type name and the chosen image info.
type instanceSpec struct {
- imageId string
- arch string
- series string
+ instanceType string
+ image image
+}
+
+// findInstanceSpec returns an instanceSpec satisfying the supplied instanceConstraint.
+func findInstanceSpec(ic *instanceConstraint) (*instanceSpec, error) {
+ images, err := getImages(ic.region, ic.series, ic.arches)
+ if err != nil {
+ return nil, err
+ }
+ itypes, err := getInstanceTypes(ic.region, ic.constraints)
+ if err != nil {
+ return nil, err
+ }
+ for _, itype := range itypes {
+ for _, image := range images {
+ if image.match(itype) {
+ return &instanceSpec{itype.name, image}, nil
+ }
+ }
+ }
+ names := make([]string, len(itypes))
+ for i, itype := range itypes {
+ names[i] = itype.name
+ }
+ return nil, fmt.Errorf("no %q images in %s matching instance types %v", ic.series, ic.region, names)
+}
+
+// image holds the attributes that vary amongst relevant images for
+// a given series in a given region.
+type image struct {
+ id string
+ arch string
+ // hvm is true when the image is built for an ec2 cluster instance type.
+ hvm bool
+}
+
+// match returns true if the image can run on the supplied instance type.
+func (image image) match(itype instanceType) bool {
+ if image.hvm != itype.hvm {
+ return false
+ }
+ for _, arch := range itype.arches {
+ if arch == image.arch {
+ return true
+ }
+ }
+ return false
}
// imagesHost holds the address of the images http server.
@@ -44,54 +93,60 @@
// + more that we don't care about.
)
-// fndInstanceSpec finds a suitable instance specification given
-// the provided constraints.
-func findInstanceSpec(spec *instanceConstraint) (*instanceSpec, error) {
- hclient := new(http.Client)
- uri := fmt.Sprintf(imagesHost+"/query/%s/%s/%s.current.txt",
- spec.series,
- "server", // variant.
- "released", // version.
- )
- resp, err := hclient.Get(uri)
- if err == nil && resp.StatusCode != 200 {
- err = fmt.Errorf("%s", resp.Status)
+// getImages returns the latest released ubuntu server images for the
+// supplied series in the supplied region.
+func getImages(region, series string, arches []string) ([]image, error) {
+ path := fmt.Sprintf("/query/%s/server/released.current.txt", series)
+ resp, err := http.Get(imagesHost + path)
+ if err == nil {
+ defer resp.Body.Close()
+ if resp.StatusCode != 200 {
+ err = fmt.Errorf("%s", resp.Status)
+ }
}
if err != nil {
- return nil, fmt.Errorf("error getting instance types: %v", err)
+ return nil, fmt.Errorf("cannot get image data for %q: %v", series, err)
}
- defer resp.Body.Close()
-
+ var images []image
r := bufio.NewReader(resp.Body)
for {
line, _, err := r.ReadLine()
- if err != nil {
- return nil, fmt.Errorf("cannot find matching image: %v", err)
+ if err == io.EOF {
+ if len(images) == 0 {
+ return nil, fmt.Errorf("no %q images in %s with arches %v", series, region, arches)
+ }
+ sort.Sort(byArch(images))
+ return images, nil
+ } else if err != nil {
+ return nil, err
}
f := strings.Split(string(line), "\t")
if len(f) < colMax {
continue
}
- if f[colVtype] == "hvm" {
+ if f[colRegion] != region {
continue
}
if f[colEBS] != "ebs" {
continue
}
- if f[colArch] == spec.arch && f[colRegion] == spec.region {
- return &instanceSpec{
- imageId: f[colImageId],
- arch: spec.arch,
- series: spec.series,
- }, nil
+ if len(filterArches([]string{f[colArch]}, arches)) != 0 {
+ images = append(images, image{
+ id: f[colImageId],
+ arch: f[colArch],
+ hvm: f[colVtype] == "hvm",
+ })
}
}
- panic("not reached")
+ panic("unreachable")
}
-func either(yes bool, a, b string) string {
- if yes {
- return a
- }
- return b
+// byArch is used to sort a slice of images by architecture preference, such
+// that amd64 images come ealier than i386 ones.
+type byArch []image
+
+func (ba byArch) Len() int { return len(ba) }
+func (ba byArch) Swap(i, j int) { ba[i], ba[j] = ba[j], ba[i] }
+func (ba byArch) Less(i, j int) bool {
+ return ba[i].arch == "amd64" && ba[j].arch != "amd64"
}
=== modified file 'environs/ec2/image_test.go'
--- environs/ec2/image_test.go 2012-08-17 06:04:24 +0000
+++ environs/ec2/image_test.go 2013-04-08 08:09:22 +0000
@@ -2,121 +2,319 @@
import (
"fmt"
- "io"
. "launchpad.net/gocheck"
- "net/http"
- "os"
- "path/filepath"
- "testing"
+ "launchpad.net/juju-core/constraints"
+ "launchpad.net/juju-core/environs/jujutest"
+ "launchpad.net/juju-core/testing"
+ "strings"
)
-type imageSuite struct{}
-
-var _ = Suite(imageSuite{})
-
-func (imageSuite) SetUpSuite(c *C) {
- UseTestImageData(true)
-}
-
-func (imageSuite) TearDownSuite(c *C) {
- UseTestImageData(false)
-}
-
-// N.B. the image IDs in this test will need updating
-// if the image directory is regenerated.
-var imageTests = []struct {
- constraint instanceConstraint
- imageId string
- err string
+type imageSuite struct {
+ testing.LoggingSuite
+}
+
+var _ = Suite(&imageSuite{})
+
+func (s *imageSuite) SetUpSuite(c *C) {
+ s.LoggingSuite.SetUpSuite(c)
+ UseTestImageData(imagesData)
+}
+
+func (s *imageSuite) TearDownSuite(c *C) {
+ UseTestImageData(nil)
+ s.LoggingSuite.TearDownTest(c)
+}
+
+var imagesData = []jujutest.FileContent{
+ {"/query/precise/server/released.current.txt", imagesFields(
+ "instance-store amd64 us-east-1 ami-00000011 paravirtual",
+ "ebs amd64 eu-west-1 ami-00000016 paravirtual",
+ "ebs i386 ap-northeast-1 ami-00000023 paravirtual",
+ "ebs amd64 ap-northeast-1 ami-00000026 paravirtual",
+ "ebs amd64 ap-northeast-1 ami-00000087 hvm",
+ "ebs amd64 test ami-00000033 paravirtual",
+ "ebs i386 test ami-00000034 paravirtual",
+ "ebs amd64 test ami-00000035 hvm",
+ )},
+ {"/query/quantal/server/released.current.txt", imagesFields(
+ "instance-store amd64 us-east-1 ami-00000011 paravirtual",
+ "ebs amd64 eu-west-1 ami-01000016 paravirtual",
+ "ebs i386 ap-northeast-1 ami-01000023 paravirtual",
+ "ebs amd64 ap-northeast-1 ami-01000026 paravirtual",
+ "ebs amd64 ap-northeast-1 ami-01000087 hvm",
+ "ebs i386 test ami-01000034 paravirtual",
+ "ebs amd64 test ami-01000035 hvm",
+ )},
+ {"/query/raring/server/released.current.txt", imagesFields(
+ "ebs i386 test ami-02000034 paravirtual",
+ )},
+}
+
+func imagesFields(srcs ...string) string {
+ strs := make([]string, len(srcs))
+ for i, src := range srcs {
+ parts := strings.Split(src, " ")
+ if len(parts) != 5 {
+ panic("bad clouddata field input")
+ }
+ args := make([]interface{}, len(parts))
+ for i, part := range parts {
+ args[i] = part
+ }
+ // Ignored fields are left empty for clarity's sake, and two additional
+ // tabs are tacked on to the end to verify extra columns are ignored.
+ strs[i] = fmt.Sprintf("\t\t\t\t%s\t%s\t%s\t%s\t\t\t%s\t\t\n", args...)
+ }
+ return strings.Join(strs, "")
+}
+
+var getImagesTests = []struct {
+ region string
+ series string
+ arches []string
+ images []image
+ err string
}{
- {instanceConstraint{
- series: "natty",
- arch: "amd64",
- region: "eu-west-1",
- }, "ami-69b28a1d", ""},
- {instanceConstraint{
- series: "natty",
- arch: "i386",
- region: "ap-northeast-1",
- }, "ami-843b8a85", ""},
- {instanceConstraint{
- series: "natty",
- arch: "amd64",
- region: "ap-northeast-1",
- }, "ami-8c3b8a8d", ""},
- {instanceConstraint{
- series: "zingy",
- arch: "amd64",
- region: "eu-west-1",
- }, "", "error getting instance types:.*"},
+ {
+ region: "us-east-1",
+ series: "precise",
+ arches: both,
+ err: `no "precise" images in us-east-1 with arches \[amd64 i386\]`,
+ }, {
+ region: "eu-west-1",
+ series: "precise",
+ arches: []string{"i386"},
+ err: `no "precise" images in eu-west-1 with arches \[i386\]`,
+ }, {
+ region: "ap-northeast-1",
+ series: "precise",
+ arches: both,
+ images: []image{
+ {"ami-00000026", "amd64", false},
+ {"ami-00000087", "amd64", true},
+ {"ami-00000023", "i386", false},
+ },
+ }, {
+ region: "ap-northeast-1",
+ series: "precise",
+ arches: []string{"amd64"},
+ images: []image{
+ {"ami-00000026", "amd64", false},
+ {"ami-00000087", "amd64", true},
+ },
+ }, {
+ region: "ap-northeast-1",
+ series: "precise",
+ arches: []string{"i386"},
+ images: []image{
+ {"ami-00000023", "i386", false},
+ },
+ }, {
+ region: "ap-northeast-1",
+ series: "quantal",
+ arches: both,
+ images: []image{
+ {"ami-01000026", "amd64", false},
+ {"ami-01000087", "amd64", true},
+ {"ami-01000023", "i386", false},
+ },
+ },
}
-func (imageSuite) TestFindInstanceSpec(c *C) {
- for i, t := range imageTests {
+func (s *imageSuite) TestGetImages(c *C) {
+ for i, t := range getImagesTests {
c.Logf("test %d", i)
- id, err := findInstanceSpec(&t.constraint)
+ images, err := getImages(t.region, t.series, t.arches)
if t.err != "" {
c.Check(err, ErrorMatches, t.err)
- c.Check(id, IsNil)
continue
}
if !c.Check(err, IsNil) {
continue
}
- if !c.Check(id, NotNil) {
- continue
- }
- c.Check(id.imageId, Equals, t.imageId)
- c.Check(id.arch, Equals, t.constraint.arch)
- c.Check(id.series, Equals, t.constraint.series)
- }
-}
-
-// regenerate all data inside the images directory.
-// N.B. this second-guesses the logic inside images.go
-func RegenerateImages(t *testing.T) {
- if err := os.RemoveAll(imagesRoot); err != nil {
- t.Errorf("cannot remove old images: %v", err)
- return
- }
- for _, variant := range []string{"desktop", "server"} {
- for _, version := range []string{"daily", "released"} {
- for _, release := range []string{"natty", "oneiric", "precise", "quantal"} {
- s := fmt.Sprintf("query/%s/%s/%s.current.txt", release, variant, version)
- t.Logf("regenerating images from %q", s)
- err := copylocal(s)
- if err != nil {
- t.Logf("regenerate: %v", err)
- }
- }
- }
- }
-}
-
-var imagesRoot = "testdata"
-
-func copylocal(s string) error {
- r, err := http.Get("http://uec-images.ubuntu.com/" + s)
- if err != nil {
- return fmt.Errorf("get %q: %v", s, err)
- }
- defer r.Body.Close()
- if r.StatusCode != 200 {
- return fmt.Errorf("status on %q: %s", s, r.Status)
- }
- path := filepath.Join(filepath.FromSlash(imagesRoot), filepath.FromSlash(s))
- d, _ := filepath.Split(path)
- if err := os.MkdirAll(d, 0777); err != nil {
- return err
- }
- file, err := os.Create(path)
- if err != nil {
- return err
- }
- defer file.Close()
- _, err = io.Copy(file, r.Body)
- if err != nil {
- return fmt.Errorf("error copying image file: %v", err)
- }
- return nil
+ c.Check(images, DeepEquals, t.images)
+ }
+}
+
+var imageMatchtests = []struct {
+ image image
+ itype instanceType
+ match bool
+}{
+ {
+ image: image{arch: "amd64"},
+ itype: instanceType{arches: []string{"amd64"}},
+ match: true,
+ }, {
+ image: image{arch: "amd64"},
+ itype: instanceType{arches: []string{"i386", "amd64"}},
+ match: true,
+ }, {
+ image: image{arch: "amd64", hvm: true},
+ itype: instanceType{arches: []string{"amd64"}, hvm: true},
+ match: true,
+ }, {
+ image: image{arch: "i386"},
+ itype: instanceType{arches: []string{"amd64"}},
+ }, {
+ image: image{arch: "amd64", hvm: true},
+ itype: instanceType{arches: []string{"amd64"}},
+ }, {
+ image: image{arch: "amd64"},
+ itype: instanceType{arches: []string{"amd64"}, hvm: true},
+ },
+}
+
+func (s *imageSuite) TestImageMatch(c *C) {
+ for i, t := range imageMatchtests {
+ c.Logf("test %d", i)
+ c.Check(t.image.match(t.itype), Equals, t.match)
+ }
+}
+
+type specSuite struct {
+ testing.LoggingSuite
+}
+
+var _ = Suite(&specSuite{})
+
+func (s *specSuite) SetUpSuite(c *C) {
+ s.LoggingSuite.SetUpSuite(c)
+ UseTestImageData(imagesData)
+ UseTestInstanceTypeData(instanceTypeData)
+}
+
+func (s *specSuite) TearDownSuite(c *C) {
+ UseTestInstanceTypeData(nil)
+ UseTestImageData(nil)
+ s.LoggingSuite.TearDownTest(c)
+}
+
+var findInstanceSpecTests = []struct {
+ series string
+ arches []string
+ cons string
+ itype string
+ image string
+}{
+ {
+ series: "precise",
+ arches: both,
+ itype: "m1.small",
+ image: "ami-00000033",
+ }, {
+ series: "quantal",
+ arches: both,
+ itype: "m1.small",
+ image: "ami-01000034",
+ }, {
+ series: "precise",
+ arches: both,
+ cons: "cpu-cores=4",
+ itype: "m1.xlarge",
+ image: "ami-00000033",
+ }, {
+ series: "precise",
+ arches: both,
+ cons: "cpu-cores=2 arch=i386",
+ itype: "c1.medium",
+ image: "ami-00000034",
+ }, {
+ series: "precise",
+ arches: both,
+ cons: "mem=10G",
+ itype: "m1.xlarge",
+ image: "ami-00000033",
+ }, {
+ series: "precise",
+ arches: both,
+ cons: "mem=",
+ itype: "m1.small",
+ image: "ami-00000033",
+ }, {
+ series: "precise",
+ arches: both,
+ cons: "cpu-power=",
+ itype: "t1.micro",
+ image: "ami-00000033",
+ }, {
+ series: "precise",
+ arches: both,
+ cons: "cpu-power=800",
+ itype: "m1.xlarge",
+ image: "ami-00000033",
+ }, {
+ series: "precise",
+ arches: both,
+ cons: "cpu-power=500 arch=i386",
+ itype: "c1.medium",
+ image: "ami-00000034",
+ }, {
+ series: "precise",
+ arches: []string{"i386"},
+ cons: "cpu-power=400",
+ itype: "c1.medium",
+ image: "ami-00000034",
+ }, {
+ series: "quantal",
+ arches: both,
+ cons: "arch=amd64",
+ itype: "cc1.4xlarge",
+ image: "ami-01000035",
+ },
+}
+
+func (s *specSuite) TestFindInstanceSpec(c *C) {
+ for i, t := range findInstanceSpecTests {
+ c.Logf("test %d", i)
+ spec, err := findInstanceSpec(&instanceConstraint{
+ region: "test",
+ series: t.series,
+ arches: t.arches,
+ constraints: constraints.MustParse(t.cons),
+ })
+ c.Assert(err, IsNil)
+ c.Check(spec.instanceType, Equals, t.itype)
+ c.Check(spec.image.id, Equals, t.image)
+ }
+}
+
+var findInstanceSpecErrorTests = []struct {
+ series string
+ arches []string
+ cons string
+ err string
+}{
+ {
+ series: "bad",
+ arches: both,
+ err: `cannot get image data for "bad": .*`,
+ }, {
+ series: "precise",
+ arches: []string{"arm"},
+ err: `no "precise" images in test with arches \[arm\]`,
+ }, {
+ series: "precise",
+ arches: both,
+ cons: "cpu-power=9001",
+ err: `no instance types in test matching constraints "cpu-power=9001"`,
+ }, {
+ series: "raring",
+ arches: both,
+ cons: "mem=4G",
+ err: `no "raring" images in test matching instance types \[m1.large m1.xlarge c1.xlarge cc1.4xlarge cc2.8xlarge\]`,
+ },
+}
+
+func (s *specSuite) TestFindInstanceSpecErrors(c *C) {
+ for i, t := range findInstanceSpecErrorTests {
+ c.Logf("test %d", i)
+ _, err := findInstanceSpec(&instanceConstraint{
+ region: "test",
+ series: t.series,
+ arches: t.arches,
+ constraints: constraints.MustParse(t.cons),
+ })
+ c.Check(err, ErrorMatches, t.err)
+ }
}
=== added file 'environs/ec2/instancetype.go'
--- environs/ec2/instancetype.go 1970-01-01 00:00:00 +0000
+++ environs/ec2/instancetype.go 2013-04-08 08:09:22 +0000
@@ -0,0 +1,367 @@
+package ec2
+
+import (
+ "fmt"
+ "launchpad.net/juju-core/constraints"
+ "sort"
+)
+
+// instanceType holds all relevant attributes of the various ec2 instance
+// types.
+type instanceType struct {
+ name string
+ arches []string
+ cpuCores uint64
+ cpuPower uint64
+ mem uint64
+ // hvm instance types must be launched with hvm images.
+ hvm bool
+}
+
+// match returns true if itype can satisfy the supplied constraints. If so,
+// it also returns a copy of itype with any arches that do not match the
+// constraints filtered out.
+func (itype instanceType) match(cons constraints.Value) (instanceType, bool) {
+ nothing := instanceType{}
+ if cons.Arch != nil {
+ itype.arches = filterArches(itype.arches, []string{*cons.Arch})
+ }
+ if len(itype.arches) == 0 {
+ return nothing, false
+ }
+ if cons.CpuCores != nil && itype.cpuCores < *cons.CpuCores {
+ return nothing, false
+ }
+ if cons.CpuPower != nil && itype.cpuPower < *cons.CpuPower {
+ return nothing, false
+ }
+ if cons.Mem != nil && itype.mem < *cons.Mem {
+ return nothing, false
+ }
+ return itype, true
+}
+
+// filterArches returns every element of src that also exists in filter.
+func filterArches(src, filter []string) (dst []string) {
+ for _, arch := range src {
+ for _, match := range filter {
+ if arch == match {
+ dst = append(dst, arch)
+ break
+ }
+ }
+ }
+ return dst
+}
+
+// defaultCpuPower is larger that t1.micro's cpuPower, and no larger than
+// any other instance type's cpuPower. It is used when no explicit CpuPower
+// constraint exists, preventing t1.micro from being chosen unless the user
+// has clearly indicated that they are willing to accept poor performance.
+var defaultCpuPower uint64 = 100
+
+// getInstanceTypes returns all instance types matching cons and available
+// in region, sorted by increasing region-specific cost.
+func getInstanceTypes(region string, cons constraints.Value) ([]instanceType, error) {
+ if cons.CpuPower == nil {
+ v := defaultCpuPower
+ cons.CpuPower = &v
+ }
+ allCosts := allRegionCosts[region]
+ if len(allCosts) == 0 {
+ return nil, fmt.Errorf("no instance types found in %s", region)
+ }
+ var costs []uint64
+ var itypes []instanceType
+ for _, itype := range allInstanceTypes {
+ cost, ok := allCosts[itype.name]
+ if !ok {
+ continue
+ }
+ itype, ok := itype.match(cons)
+ if !ok {
+ continue
+ }
+ costs = append(costs, cost)
+ itypes = append(itypes, itype)
+ }
+ if len(itypes) == 0 {
+ return nil, fmt.Errorf("no instance types in %s matching constraints %q", region, cons)
+ }
+ sort.Sort(byCost{itypes, costs})
+ return itypes, nil
+}
+
+// byCost is used to sort a slice of instance types as a side effect of
+// sorting a matching slice of costs in USDe-3/hour.
+type byCost struct {
+ itypes []instanceType
+ costs []uint64
+}
+
+func (bc byCost) Len() int { return len(bc.costs) }
+func (bc byCost) Less(i, j int) bool { return bc.costs[i] < bc.costs[j] }
+func (bc byCost) Swap(i, j int) {
+ bc.costs[i], bc.costs[j] = bc.costs[j], bc.costs[i]
+ bc.itypes[i], bc.itypes[j] = bc.itypes[j], bc.itypes[i]
+}
+
+// all instance types can run amd64 images, and some can also run i386 ones.
+var (
+ amd64 = []string{"amd64"}
+ both = []string{"amd64", "i386"}
+)
+
+// allInstanceTypes holds the relevant attributes of every known
+// instance type.
+var allInstanceTypes = []instanceType{
+ { // First generation.
+ name: "m1.small",
+ arches: both,
+ cpuCores: 1,
+ cpuPower: 100,
+ mem: 1740,
+ }, {
+ name: "m1.medium",
+ arches: both,
+ cpuCores: 1,
+ cpuPower: 200,
+ mem: 3840,
+ }, {
+ name: "m1.large",
+ arches: amd64,
+ cpuCores: 2,
+ cpuPower: 400,
+ mem: 7680,
+ }, {
+ name: "m1.xlarge",
+ arches: amd64,
+ cpuCores: 4,
+ cpuPower: 800,
+ mem: 15360,
+ },
+ { // Second generation.
+ name: "m3.xlarge",
+ arches: amd64,
+ cpuCores: 4,
+ cpuPower: 1300,
+ mem: 15360,
+ }, {
+ name: "m3.2xlarge",
+ arches: amd64,
+ cpuCores: 8,
+ cpuPower: 2600,
+ mem: 30720,
+ },
+ { // Micro.
+ name: "t1.micro",
+ arches: both,
+ cpuCores: 1,
+ cpuPower: 20,
+ mem: 613,
+ },
+ { // High-memory.
+ name: "m2.xlarge",
+ arches: amd64,
+ cpuCores: 2,
+ cpuPower: 650,
+ mem: 17408,
+ }, {
+ name: "m2.2xlarge",
+ arches: amd64,
+ cpuCores: 4,
+ cpuPower: 1300,
+ mem: 34816,
+ }, {
+ name: "m2.4xlarge",
+ arches: amd64,
+ cpuCores: 8,
+ cpuPower: 2600,
+ mem: 69632,
+ },
+ { // High-CPU.
+ name: "c1.medium",
+ arches: both,
+ cpuCores: 2,
+ cpuPower: 500,
+ mem: 1740,
+ }, {
+ name: "c1.xlarge",
+ arches: amd64,
+ cpuCores: 8,
+ cpuPower: 2000,
+ mem: 7168,
+ },
+ { // Cluster compute.
+ name: "cc1.4xlarge",
+ arches: amd64,
+ cpuCores: 8,
+ cpuPower: 3350,
+ mem: 23552,
+ hvm: true,
+ }, {
+ name: "cc2.8xlarge",
+ arches: amd64,
+ cpuCores: 16,
+ cpuPower: 8800,
+ mem: 61952,
+ hvm: true,
+ },
+ { // High memory cluster.
+ name: "cr1.8xlarge",
+ arches: amd64,
+ cpuCores: 16,
+ cpuPower: 8800,
+ mem: 249856,
+ hvm: true,
+ },
+ { // Cluster GPU.
+ name: "cg1.4xlarge",
+ arches: amd64,
+ cpuCores: 8,
+ cpuPower: 3350,
+ mem: 22528,
+ hvm: true,
+ },
+ { // High I/O.
+ name: "hi1.4xlarge",
+ arches: amd64,
+ cpuCores: 16,
+ cpuPower: 3500,
+ mem: 61952,
+ },
+ { // High storage.
+ name: "hs1.8xlarge",
+ arches: amd64,
+ cpuCores: 16,
+ cpuPower: 3500,
+ mem: 119808,
+ },
+}
+
+// allRegionCosts holds the cost in USDe-3/hour for each available instance
+// type in each region.
+var allRegionCosts = map[string]map[string]uint64{
+ "ap-northeast-1": { // Tokyo.
+ "m1.small": 88,
+ "m1.medium": 175,
+ "m1.large": 350,
+ "m1.xlarge": 700,
+ "m3.xlarge": 760,
+ "m3.2xlarge": 1520,
+ "t1.micro": 27,
+ "m2.xlarge": 505,
+ "m2.2xlarge": 1010,
+ "m2.4xlarge": 2020,
+ "c1.medium": 185,
+ "c1.xlarge": 740,
+ },
+ "ap-southeast-1": { // Singapore.
+ "m1.small": 80,
+ "m1.medium": 160,
+ "m1.large": 320,
+ "m1.xlarge": 640,
+ "m3.xlarge": 700,
+ "m3.2xlarge": 1400,
+ "t1.micro": 020,
+ "m2.xlarge": 495,
+ "m2.2xlarge": 990,
+ "m2.4xlarge": 1980,
+ "c1.medium": 183,
+ "c1.xlarge": 730,
+ },
+ "ap-southeast-2": { // Sydney.
+ "m1.small": 80,
+ "m1.medium": 160,
+ "m1.large": 320,
+ "m1.xlarge": 640,
+ "m3.xlarge": 700,
+ "m3.2xlarge": 1400,
+ "t1.micro": 020,
+ "m2.xlarge": 495,
+ "m2.2xlarge": 990,
+ "m2.4xlarge": 1980,
+ "c1.medium": 183,
+ "c1.xlarge": 730,
+ },
+ "eu-west-1": { // Ireland.
+ "m1.small": 65,
+ "m1.medium": 130,
+ "m1.large": 260,
+ "m1.xlarge": 520,
+ "m3.xlarge": 550,
+ "m3.2xlarge": 1100,
+ "t1.micro": 020,
+ "m2.xlarge": 460,
+ "m2.2xlarge": 920,
+ "m2.4xlarge": 1840,
+ "c1.medium": 165,
+ "c1.xlarge": 660,
+ "cc2.8xlarge": 2700,
+ "cg1.4xlarge": 2360,
+ "hi1.4xlarge": 3410,
+ },
+ "sa-east-1": { // Sao Paulo.
+ "m1.small": 80,
+ "m1.medium": 160,
+ "m1.large": 320,
+ "m1.xlarge": 640,
+ "t1.micro": 027,
+ "m2.xlarge": 540,
+ "m2.2xlarge": 1080,
+ "m2.4xlarge": 2160,
+ "c1.medium": 200,
+ "c1.xlarge": 800,
+ },
+ "us-east-1": { // Northern Virginia.
+ "m1.small": 60,
+ "m1.medium": 120,
+ "m1.large": 240,
+ "m1.xlarge": 480,
+ "m3.xlarge": 500,
+ "m3.2xlarge": 1000,
+ "t1.micro": 20,
+ "m2.xlarge": 410,
+ "m2.2xlarge": 820,
+ "m2.4xlarge": 1640,
+ "c1.medium": 145,
+ "c1.xlarge": 580,
+ "cc1.4xlarge": 1300,
+ "cc2.8xlarge": 2400,
+ "cr1.8xlarge": 3500,
+ "cg1.4xlarge": 2100,
+ "hi1.4xlarge": 3100,
+ "hs1.8xlarge": 4600,
+ },
+ "us-west-1": { // Northern California.
+ "m1.small": 65,
+ "m1.medium": 130,
+ "m1.large": 260,
+ "m1.xlarge": 520,
+ "m3.xlarge": 550,
+ "m3.2xlarge": 1100,
+ "t1.micro": 25,
+ "m2.xlarge": 460,
+ "m2.2xlarge": 920,
+ "m2.4xlarge": 1840,
+ "c1.medium": 165,
+ "c1.xlarge": 660,
+ },
+ "us-west-2": { // Oregon.
+ "m1.small": 60,
+ "m1.medium": 120,
+ "m1.large": 240,
+ "m1.xlarge": 480,
+ "m3.xlarge": 500,
+ "m3.2xlarge": 1000,
+ "t1.micro": 020,
+ "m2.xlarge": 410,
+ "m2.2xlarge": 820,
+ "m2.4xlarge": 1640,
+ "c1.medium": 145,
+ "c1.xlarge": 580,
+ "cc2.8xlarge": 2400,
+ "cr1.8xlarge": 3500,
+ "hi1.4xlarge": 3100,
+ },
+}
=== added file 'environs/ec2/instancetype_test.go'
--- environs/ec2/instancetype_test.go 1970-01-01 00:00:00 +0000
+++ environs/ec2/instancetype_test.go 2013-04-08 08:09:22 +0000
@@ -0,0 +1,162 @@
+package ec2
+
+import (
+ . "launchpad.net/gocheck"
+ "launchpad.net/juju-core/constraints"
+ "launchpad.net/juju-core/testing"
+)
+
+type instanceTypeSuite struct {
+ testing.LoggingSuite
+}
+
+var _ = Suite(&instanceTypeSuite{})
+
+func (s *instanceTypeSuite) SetUpSuite(c *C) {
+ s.LoggingSuite.SetUpSuite(c)
+ UseTestInstanceTypeData(instanceTypeData)
+}
+
+func (s *instanceTypeSuite) TearDownSuite(c *C) {
+ UseTestInstanceTypeData(nil)
+ s.LoggingSuite.TearDownTest(c)
+}
+
+var instanceTypeData = map[string]uint64{
+ "m1.small": 60,
+ "m1.medium": 120,
+ "m1.large": 240,
+ "m1.xlarge": 480,
+ "t1.micro": 20,
+ "c1.medium": 145,
+ "c1.xlarge": 580,
+ "cc1.4xlarge": 1300,
+ "cc2.8xlarge": 2400,
+}
+
+var getInstanceTypesTest = []struct {
+ info string
+ cons string
+ itypes []string
+ arches []string
+}{
+ {
+ info: "cpu-cores",
+ cons: "cpu-cores=2",
+ itypes: []string{
+ "c1.medium", "m1.large", "m1.xlarge", "c1.xlarge", "cc1.4xlarge",
+ "cc2.8xlarge",
+ },
+ }, {
+ info: "cpu-power",
+ cons: "cpu-power=2000",
+ itypes: []string{"c1.xlarge", "cc1.4xlarge", "cc2.8xlarge"},
+ }, {
+ info: "mem",
+ cons: "mem=4G",
+ itypes: []string{
+ "m1.large", "m1.xlarge", "c1.xlarge", "cc1.4xlarge", "cc2.8xlarge",
+ },
+ }, {
+ info: "arches filtered by constraint",
+ cons: "arch=i386",
+ itypes: []string{"m1.small", "m1.medium", "c1.medium"},
+ arches: []string{"i386"},
+ }, {
+ info: "t1.micro filtered out when no cpu-power set",
+ itypes: []string{
+ "m1.small", "m1.medium", "c1.medium", "m1.large", "m1.xlarge",
+ "c1.xlarge", "cc1.4xlarge", "cc2.8xlarge",
+ },
+ }, {
+ info: "t1.micro included when small cpu-power set",
+ cons: "cpu-power=1",
+ itypes: []string{
+ "t1.micro", "m1.small", "m1.medium", "c1.medium", "m1.large",
+ "m1.xlarge", "c1.xlarge", "cc1.4xlarge", "cc2.8xlarge",
+ },
+ }, {
+ info: "t1.micro included when small cpu-power set 2",
+ cons: "cpu-power=1 arch=i386",
+ itypes: []string{"t1.micro", "m1.small", "m1.medium", "c1.medium"},
+ arches: []string{"i386"},
+ },
+}
+
+func (s *instanceTypeSuite) TestGetInstanceTypes(c *C) {
+ for i, t := range getInstanceTypesTest {
+ c.Logf("test %d: %s", i, t.info)
+ itypes, err := getInstanceTypes("test", constraints.MustParse(t.cons))
+ c.Assert(err, IsNil)
+ names := make([]string, len(itypes))
+ for i, itype := range itypes {
+ if len(t.arches) > 0 {
+ c.Check(itype.arches, DeepEquals, filterArches(itype.arches, t.arches))
+ } else {
+ c.Check(len(itype.arches) > 0, Equals, true)
+ }
+ names[i] = itype.name
+ }
+ c.Check(names, DeepEquals, t.itypes)
+ }
+}
+
+func (s *instanceTypeSuite) TestGetInstanceTypesErrors(c *C) {
+ _, err := getInstanceTypes("unknown-region", constraints.Value{})
+ c.Check(err, ErrorMatches, `no instance types found in unknown-region`)
+
+ cons := constraints.MustParse("cpu-power=9001")
+ _, err = getInstanceTypes("test", cons)
+ c.Check(err, ErrorMatches, `no instance types in test matching constraints "cpu-power=9001"`)
+
+ cons = constraints.MustParse("arch=i386 mem=8G")
+ _, err = getInstanceTypes("test", cons)
+ c.Check(err, ErrorMatches, `no instance types in test matching constraints "arch=i386 cpu-power=100 mem=8192M"`)
+}
+
+var instanceTypeMatchTests = []struct {
+ cons string
+ itype string
+ arches []string
+}{
+ {"", "m1.small", both},
+ {"", "m1.large", amd64},
+ {"cpu-power=100", "m1.small", both},
+ {"arch=amd64", "m1.small", amd64},
+ {"cpu-cores=3", "m1.xlarge", amd64},
+ {"cpu-power=", "t1.micro", both},
+ {"cpu-power=500", "c1.medium", both},
+ {"cpu-power=2000", "c1.xlarge", amd64},
+ {"cpu-power=2001", "cc1.4xlarge", amd64},
+ {"mem=2G", "m1.medium", both},
+
+ {"arch=arm", "m1.small", nil},
+ {"cpu-power=100", "t1.micro", nil},
+ {"cpu-power=9001", "cc2.8xlarge", nil},
+ {"mem=1G", "t1.micro", nil},
+ {"arch=i386", "c1.xlarge", nil},
+}
+
+func (s *instanceTypeSuite) TestMatch(c *C) {
+ for i, t := range instanceTypeMatchTests {
+ c.Logf("test %d", i)
+ cons := constraints.MustParse(t.cons)
+ var itype instanceType
+ for _, itype = range allInstanceTypes {
+ if itype.name == t.itype {
+ break
+ }
+ }
+ c.Assert(itype.name, Not(Equals), "")
+ itype, match := itype.match(cons)
+ if len(t.arches) > 0 {
+ c.Check(match, Equals, true)
+ expect := itype
+ expect.arches = t.arches
+ c.Check(itype, DeepEquals, expect)
+ } else {
+ c.Check(match, Equals, false)
+ c.Check(itype, DeepEquals, instanceType{})
+ }
+ }
+}
=== modified file 'environs/ec2/live_test.go'
--- environs/ec2/live_test.go 2013-01-24 14:15:08 +0000
+++ environs/ec2/live_test.go 2013-04-08 08:09:22 +0000
@@ -7,9 +7,12 @@
"io/ioutil"
amzec2 "launchpad.net/goamz/ec2"
. "launchpad.net/gocheck"
+ "launchpad.net/juju-core/constraints"
"launchpad.net/juju-core/environs"
+ "launchpad.net/juju-core/environs/config"
"launchpad.net/juju-core/environs/ec2"
"launchpad.net/juju-core/environs/jujutest"
+ envtesting "launchpad.net/juju-core/environs/testing"
"launchpad.net/juju-core/juju/testing"
"launchpad.net/juju-core/state"
coretesting "launchpad.net/juju-core/testing"
@@ -48,7 +51,7 @@
}
Suite(&LiveTests{
LiveTests: jujutest.LiveTests{
- Config: attrs,
+ TestConfig: jujutest.TestConfig{attrs},
Attempt: *ec2.ShortAttempt,
CanOpenState: true,
HasProvisioner: true,
@@ -66,7 +69,8 @@
func (t *LiveTests) SetUpSuite(c *C) {
t.LoggingSuite.SetUpSuite(c)
- e, err := environs.NewFromAttrs(t.Config)
+ // TODO: Share code from jujutest.LiveTests for creating environment
+ e, err := environs.NewFromAttrs(t.TestConfig.Config)
c.Assert(err, IsNil)
// Environ.PublicStorage() is read only.
@@ -76,7 +80,7 @@
// Put some fake tools in place so that tests that are simply
// starting instances without any need to check if those instances
// are running will find them in the public bucket.
- putFakeTools(c, t.writablePublicStorage)
+ envtesting.PutFakeTools(c, t.writablePublicStorage)
t.LiveTests.SetUpSuite(c)
}
@@ -103,9 +107,8 @@
// TODO(niemeyer): Looks like many of those tests should be moved to jujutest.LiveTests.
-func (t *LiveTests) TestInstanceDNSName(c *C) {
- inst, err := t.Env.StartInstance("30", testing.InvalidStateInfo("30"), testing.InvalidAPIInfo("30"), nil)
- c.Assert(err, IsNil)
+func (t *LiveTests) TestInstanceAttributes(c *C) {
+ inst := testing.StartInstance(c, t.Env, "30")
defer t.Env.StopInstances([]environs.Instance{inst})
dns, err := inst.WaitDNSName()
// TODO(niemeyer): This assert sometimes fails with "no instances found"
@@ -118,6 +121,16 @@
ec2inst := ec2.InstanceEC2(insts[0])
c.Assert(ec2inst.DNSName, Equals, dns)
+ c.Assert(ec2inst.InstanceType, Equals, "m1.small")
+}
+
+func (t *LiveTests) TestStartInstanceConstraints(c *C) {
+ cons := constraints.MustParse("mem=2G")
+ inst, err := t.Env.StartInstance("31", config.DefaultSeries, cons, testing.InvalidStateInfo("31"), testing.InvalidAPIInfo("31"))
+ c.Assert(err, IsNil)
+ defer t.Env.StopInstances([]environs.Instance{inst})
+ ec2inst := ec2.InstanceEC2(inst)
+ c.Assert(ec2inst.InstanceType, Equals, "m1.medium")
}
func (t *LiveTests) TestInstanceGroups(c *C) {
@@ -155,16 +168,14 @@
})
c.Assert(err, IsNil)
- inst0, err := t.Env.StartInstance("98", testing.InvalidStateInfo("98"), testing.InvalidAPIInfo("98"), nil)
- c.Assert(err, IsNil)
+ inst0 := testing.StartInstance(c, t.Env, "98")
defer t.Env.StopInstances([]environs.Instance{inst0})
// Create a same-named group for the second instance
// before starting it, to check that it's reused correctly.
oldMachineGroup := createGroup(c, ec2conn, groups[2].Name, "old machine group")
- inst1, err := t.Env.StartInstance("99", testing.InvalidStateInfo("99"), testing.InvalidAPIInfo("99"), nil)
- c.Assert(err, IsNil)
+ inst1 := testing.StartInstance(c, t.Env, "99")
defer t.Env.StopInstances([]environs.Instance{inst1})
groupsResp, err := ec2conn.SecurityGroups(groups, nil)
@@ -295,15 +306,11 @@
// It would be nice if this test was in jujutest, but
// there's no way for jujutest to fabricate a valid-looking
// instance id.
- inst0, err := t.Env.StartInstance("40", testing.InvalidStateInfo("40"), testing.InvalidAPIInfo("40"), nil)
- c.Assert(err, IsNil)
-
+ inst0 := testing.StartInstance(c, t.Env, "40")
inst1 := ec2.FabricateInstance(inst0, "i-aaaaaaaa")
-
- inst2, err := t.Env.StartInstance("41", testing.InvalidStateInfo("41"), testing.InvalidAPIInfo("41"), nil)
- c.Assert(err, IsNil)
-
- err = t.Env.StopInstances([]environs.Instance{inst0, inst1, inst2})
+ inst2 := testing.StartInstance(c, t.Env, "41")
+
+ err := t.Env.StopInstances([]environs.Instance{inst0, inst1, inst2})
c.Check(err, IsNil)
var insts []environs.Instance
=== modified file 'environs/ec2/local_test.go'
--- environs/ec2/local_test.go 2013-02-27 13:28:43 +0000
+++ environs/ec2/local_test.go 2013-04-08 08:09:22 +0000
@@ -9,35 +9,68 @@
"launchpad.net/goamz/s3/s3test"
. "launchpad.net/gocheck"
"launchpad.net/goyaml"
+ "launchpad.net/juju-core/constraints"
"launchpad.net/juju-core/environs"
+ "launchpad.net/juju-core/environs/config"
"launchpad.net/juju-core/environs/ec2"
"launchpad.net/juju-core/environs/jujutest"
+ envtesting "launchpad.net/juju-core/environs/testing"
"launchpad.net/juju-core/state"
"launchpad.net/juju-core/testing"
"launchpad.net/juju-core/trivial"
- "launchpad.net/juju-core/version"
"regexp"
- "strings"
)
type ProviderSuite struct{}
var _ = Suite(&ProviderSuite{})
+var testImagesContent = []jujutest.FileContent{{
+ Name: "/query/precise/server/released.current.txt",
+ Content: "" +
+ "precise\tserver\trelease\t20121017\tebs\tamd64\ttest\tami-20800c10\taki-98e26fa8\t\tparavirtual\n" +
+ "precise\tserver\trelease\t20121017\tebs\ti386\ttest\tami-00000034\tparavirtual\n",
+}, {
+ Name: "/query/quantal/server/released.current.txt",
+ Content: "" +
+ "quantal\tserver\trelease\t20121017\tebs\tamd64\ttest\tami-40f97070\taki-98e26fa8\t\tparavirtual\n" +
+ "quantal\tserver\trelease\t20121017\tebs\ti386\ttest\tami-01000034\taki-98e26fa8\t\tparavirtual\n",
+}, {
+ Name: "/query/raring/server/released.current.txt",
+ Content: "" +
+ "raring\tserver\trelease\t20121017\tebs\tamd64\ttest\tami-40f97070\taki-98e26fa8\t\tparavirtual\n" +
+ "raring\tserver\trelease\t20121017\tebs\ti386\ttest\tami-40f97070\taki-98e26fa8\t\tparavirtual\n",
+}}
+
+// testInstanceTypeContent holds the cost in USDe-3/hour for each of the
+// few available instance types in the convenient fictional "test" region.
+var testInstanceTypeContent = map[string]uint64{
+ "m1.small": 60,
+ "m1.medium": 120,
+ "m1.large": 240,
+ "m1.xlarge": 480,
+ "t1.micro": 020,
+}
+
func (s *ProviderSuite) TestMetadata(c *C) {
- ec2.UseTestMetadata(true)
- defer ec2.UseTestMetadata(false)
+ metadataContent := []jujutest.FileContent{
+ {"/2011-01-01/meta-data/instance-id", "dummy.instance.id"},
+ {"/2011-01-01/meta-data/public-hostname", "public.dummy.address.invalid"},
+ {"/2011-01-01/meta-data/local-hostname", "private.dummy.address.invalid"},
+ }
+ ec2.UseTestMetadata(metadataContent)
+ defer ec2.UseTestMetadata(nil)
p, err := environs.Provider("ec2")
c.Assert(err, IsNil)
addr, err := p.PublicAddress()
c.Assert(err, IsNil)
- c.Assert(addr, Equals, "public.dummy.address.example.com")
+ c.Assert(addr, Equals, "public.dummy.address.invalid")
addr, err = p.PrivateAddress()
c.Assert(err, IsNil)
- c.Assert(addr, Equals, "private.dummy.address.example.com")
+ c.Assert(addr, Equals, "private.dummy.address.invalid")
id, err := p.InstanceId()
c.Assert(err, IsNil)
@@ -67,19 +100,19 @@
Suite(&localServerSuite{
Tests: jujutest.Tests{
- Config: attrs,
+ TestConfig: jujutest.TestConfig{attrs},
},
})
Suite(&localLiveSuite{
LiveTests: LiveTests{
LiveTests: jujutest.LiveTests{
- Config: attrs,
+ TestConfig: jujutest.TestConfig{attrs},
},
},
})
Suite(&localNonUSEastSuite{
tests: jujutest.Tests{
- Config: attrs,
+ TestConfig: jujutest.TestConfig{attrs},
},
srv: localServer{
config: &s3test.Config{
@@ -100,7 +133,8 @@
func (t *localLiveSuite) SetUpSuite(c *C) {
t.LoggingSuite.SetUpSuite(c)
- ec2.UseTestImageData(true)
+ ec2.UseTestImageData(testImagesContent)
+ ec2.UseTestInstanceTypeData(testInstanceTypeContent)
t.srv.startServer(c)
t.LiveTests.SetUpSuite(c)
t.env = t.LiveTests.Env
@@ -112,7 +146,8 @@
t.srv.stopServer(c)
t.env = nil
ec2.ShortTimeouts(false)
- ec2.UseTestImageData(false)
+ ec2.UseTestImageData(nil)
+ ec2.UseTestInstanceTypeData(nil)
t.LoggingSuite.TearDownSuite(c)
}
@@ -151,22 +186,10 @@
S3LocationConstraint: true,
}
s3inst := s3.New(aws.Auth{}, aws.Regions["test"])
- putFakeTools(c, ec2.BucketStorage(s3inst.Bucket("public-tools")))
+ envtesting.PutFakeTools(c, ec2.BucketStorage(s3inst.Bucket("public-tools")))
srv.addSpice(c)
}
-// putFakeTools sets up a bucket containing something
-// that looks like a tools archive so test methods
-// that start an instance can succeed even though they
-// do not upload tools.
-func putFakeTools(c *C, s environs.StorageWriter) {
- path := environs.ToolsStoragePath(version.Current)
- c.Logf("putting fake tools at %v", path)
- toolsContents := "tools archive, honest guv"
- err := s.Put(path, strings.NewReader(toolsContents), int64(len(toolsContents)))
- c.Assert(err, IsNil)
-}
-
// addSpice adds some "spice" to the local server
// by adding state that may cause tests to fail.
func (srv *localServer) addSpice(c *C) {
@@ -203,7 +226,8 @@
func (t *localServerSuite) SetUpSuite(c *C) {
t.LoggingSuite.SetUpSuite(c)
- ec2.UseTestImageData(true)
+ ec2.UseTestImageData(testImagesContent)
+ ec2.UseTestInstanceTypeData(testInstanceTypeContent)
t.Tests.SetUpSuite(c)
ec2.ShortTimeouts(true)
}
@@ -211,7 +235,8 @@
func (t *localServerSuite) TearDownSuite(c *C) {
t.Tests.TearDownSuite(c)
ec2.ShortTimeouts(false)
- ec2.UseTestImageData(false)
+ ec2.UseTestImageData(nil)
+ ec2.UseTestInstanceTypeData(nil)
t.LoggingSuite.TearDownSuite(c)
}
@@ -228,26 +253,24 @@
t.LoggingSuite.TearDownTest(c)
}
-func panicWrite(name string, cert, key []byte) error {
- panic("writeCertAndKey called unexpectedly")
-}
-
func (t *localServerSuite) TestBootstrapInstanceUserDataAndState(c *C) {
policy := t.env.AssignmentPolicy()
- c.Assert(policy, Equals, state.AssignUnused)
+ c.Assert(policy, Equals, state.AssignNew)
- err := environs.Bootstrap(t.env, true, panicWrite)
+ _, err := environs.PutTools(t.env.Storage(), nil)
+ c.Assert(err, IsNil)
+ err = environs.Bootstrap(t.env, constraints.Value{})
c.Assert(err, IsNil)
// check that the state holds the id of the bootstrap machine.
- state, err := ec2.LoadState(t.env)
+ bootstrapState, err := ec2.LoadState(t.env)
c.Assert(err, IsNil)
- c.Assert(state.StateInstances, HasLen, 1)
+ c.Assert(bootstrapState.StateInstances, HasLen, 1)
- insts, err := t.env.Instances(state.StateInstances)
+ insts, err := t.env.Instances(bootstrapState.StateInstances)
c.Assert(err, IsNil)
c.Assert(insts, HasLen, 1)
- c.Check(insts[0].Id(), Equals, state.StateInstances[0])
+ c.Check(insts[0].Id(), Equals, bootstrapState.StateInstances[0])
info, apiInfo, err := t.env.StateInfo()
c.Assert(err, IsNil)
@@ -275,9 +298,10 @@
// check that a new instance will be started without
// zookeeper, with a machine agent, and without a
// provisioning agent.
- info.EntityName = "machine-1"
- apiInfo.EntityName = "machine-1"
- inst1, err := t.env.StartInstance("1", info, apiInfo, nil)
+ series := config.DefaultSeries
+ info.Tag = "machine-1"
+ apiInfo.Tag = "machine-1"
+ inst1, err := t.env.StartInstance("1", series, constraints.Value{}, info, apiInfo)
c.Assert(err, IsNil)
inst = t.srv.ec2srv.Instance(string(inst1.Id()))
c.Assert(inst, NotNil)
@@ -365,14 +389,16 @@
func (t *localNonUSEastSuite) SetUpSuite(c *C) {
t.LoggingSuite.SetUpSuite(c)
- ec2.UseTestImageData(true)
+ ec2.UseTestImageData(testImagesContent)
+ ec2.UseTestInstanceTypeData(testInstanceTypeContent)
t.tests.SetUpSuite(c)
ec2.ShortTimeouts(true)
}
func (t *localNonUSEastSuite) TearDownSuite(c *C) {
ec2.ShortTimeouts(false)
- ec2.UseTestImageData(false)
+ ec2.UseTestImageData(nil)
+ ec2.UseTestInstanceTypeData(nil)
t.LoggingSuite.TearDownSuite(c)
}
=== modified file 'environs/ec2/storage.go'
--- environs/ec2/storage.go 2012-12-21 06:56:22 +0000
+++ environs/ec2/storage.go 2013-04-08 08:09:22 +0000
@@ -56,10 +56,9 @@
func (s *storage) Get(file string) (r io.ReadCloser, err error) {
for a := shortAttempt.Start(); a.Next(); {
r, err = s.bucket.GetReader(file)
- if s3ErrorStatusCode(err) == 404 {
- continue
+ if s3ErrorStatusCode(err) != 404 {
+ break
}
- return
}
return r, maybeNotFound(err)
}
@@ -156,7 +155,7 @@
}
func maybeNotFound(err error) error {
- if s3ErrorStatusCode(err) == 404 {
+ if err != nil && s3ErrorStatusCode(err) == 404 {
return &environs.NotFoundError{err}
}
return err
=== modified file 'environs/ec2/suite_test.go'
--- environs/ec2/suite_test.go 2012-07-10 00:12:25 +0000
+++ environs/ec2/suite_test.go 2013-04-08 08:09:22 +0000
@@ -3,17 +3,12 @@
import (
"flag"
. "launchpad.net/gocheck"
- "launchpad.net/juju-core/environs/ec2"
"testing"
)
-var regenerate = flag.Bool("regenerate-images", false, "regenerate all data in images directory")
var amazon = flag.Bool("amazon", false, "Also run some tests on live Amazon servers")
func TestEC2(t *testing.T) {
- if *regenerate {
- ec2.RegenerateImages(t)
- }
if *amazon {
registerAmazonTests()
}
=== removed directory 'environs/ec2/testdata'
=== removed directory 'environs/ec2/testdata/2011-01-01'
=== removed directory 'environs/ec2/testdata/2011-01-01/meta-data'
=== removed file 'environs/ec2/testdata/2011-01-01/meta-data/instance-id'
--- environs/ec2/testdata/2011-01-01/meta-data/instance-id 2013-02-27 13:28:43 +0000
+++ environs/ec2/testdata/2011-01-01/meta-data/instance-id 1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
-dummy.instance.id
=== removed file 'environs/ec2/testdata/2011-01-01/meta-data/local-hostname'
--- environs/ec2/testdata/2011-01-01/meta-data/local-hostname 2012-08-17 06:04:24 +0000
+++ environs/ec2/testdata/2011-01-01/meta-data/local-hostname 1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
-private.dummy.address.example.com
=== removed file 'environs/ec2/testdata/2011-01-01/meta-data/public-hostname'
--- environs/ec2/testdata/2011-01-01/meta-data/public-hostname 2012-08-17 06:04:24 +0000
+++ environs/ec2/testdata/2011-01-01/meta-data/public-hostname 1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
-public.dummy.address.example.com
=== removed directory 'environs/ec2/testdata/query'
=== removed directory 'environs/ec2/testdata/query/natty'
=== removed directory 'environs/ec2/testdata/query/natty/desktop'
=== removed file 'environs/ec2/testdata/query/natty/desktop/daily.current.txt'
--- environs/ec2/testdata/query/natty/desktop/daily.current.txt 2012-05-24 16:09:11 +0000
+++ environs/ec2/testdata/query/natty/desktop/daily.current.txt 1970-01-01 00:00:00 +0000
@@ -1,29 +0,0 @@
-natty desktop daily 20120521 ebs amd64 ap-northeast-1 ami-062b9b07 aki-d409a2d5 paravirtual
-natty desktop daily 20120521 ebs i386 ap-northeast-1 ami-fc2a9afd aki-d209a2d3 paravirtual
-natty desktop daily 20120521 instance-store amd64 ap-northeast-1 ami-a02a9aa1 aki-d409a2d5 paravirtual
-natty desktop daily 20120521 instance-store i386 ap-northeast-1 ami-6c2a9a6d aki-d209a2d3 paravirtual
-natty desktop daily 20120521 ebs amd64 ap-southeast-1 ami-c6501694 aki-11d5aa43 paravirtual
-natty desktop daily 20120521 ebs i386 ap-southeast-1 ami-d4501686 aki-13d5aa41 paravirtual
-natty desktop daily 20120521 instance-store amd64 ap-southeast-1 ami-62501630 aki-11d5aa43 paravirtual
-natty desktop daily 20120521 instance-store i386 ap-southeast-1 ami-ba5315e8 aki-13d5aa41 paravirtual
-natty desktop daily 20120521 ebs amd64 eu-west-1 ami-e5d2e891 aki-4feec43b paravirtual
-natty desktop daily 20120521 ebs i386 eu-west-1 ami-1fd2e86b aki-4deec439 paravirtual
-natty desktop daily 20120521 instance-store amd64 eu-west-1 ami-b1dde7c5 aki-4feec43b paravirtual
-natty desktop daily 20120521 instance-store i386 eu-west-1 ami-43dde737 aki-4deec439 paravirtual
-natty desktop daily 20120521 ebs amd64 sa-east-1 ami-54ee3049 aki-d63ce3cb paravirtual
-natty desktop daily 20120521 ebs i386 sa-east-1 ami-5eee3043 aki-863ce39b paravirtual
-natty desktop daily 20120521 instance-store amd64 sa-east-1 ami-2cee3031 aki-d63ce3cb paravirtual
-natty desktop daily 20120521 instance-store i386 sa-east-1 ami-3eee3023 aki-863ce39b paravirtual
-natty desktop daily 20120521 ebs amd64 us-east-1 ami-f43e999d hvm
-natty desktop daily 20120521 ebs amd64 us-east-1 ami-0a3e9963 aki-427d952b paravirtual
-natty desktop daily 20120521 ebs i386 us-east-1 ami-763e991f aki-407d9529 paravirtual
-natty desktop daily 20120521 instance-store amd64 us-east-1 ami-2c3f9845 aki-427d952b paravirtual
-natty desktop daily 20120521 instance-store i386 us-east-1 ami-5e309737 aki-407d9529 paravirtual
-natty desktop daily 20120521 ebs amd64 us-west-1 ami-2f306a6a aki-9ba0f1de paravirtual
-natty desktop daily 20120521 ebs i386 us-west-1 ami-07306a42 aki-99a0f1dc paravirtual
-natty desktop daily 20120521 instance-store amd64 us-west-1 ami-ab3369ee aki-9ba0f1de paravirtual
-natty desktop daily 20120521 instance-store i386 us-west-1 ami-973369d2 aki-99a0f1dc paravirtual
-natty desktop daily 20120521 ebs amd64 us-west-2 ami-c841cdf8 aki-ace26f9c paravirtual
-natty desktop daily 20120521 ebs i386 us-west-2 ami-c641cdf6 aki-dce26fec paravirtual
-natty desktop daily 20120521 instance-store amd64 us-west-2 ami-ee41cdde aki-ace26f9c paravirtual
-natty desktop daily 20120521 instance-store i386 us-west-2 ami-fe41cdce aki-dce26fec paravirtual
=== removed directory 'environs/ec2/testdata/query/natty/server'
=== removed file 'environs/ec2/testdata/query/natty/server/daily.current.txt'
--- environs/ec2/testdata/query/natty/server/daily.current.txt 2012-05-24 16:09:11 +0000
+++ environs/ec2/testdata/query/natty/server/daily.current.txt 1970-01-01 00:00:00 +0000
@@ -1,29 +0,0 @@
-natty server daily 20120524 ebs amd64 ap-northeast-1 ami-6200b063 aki-d409a2d5 paravirtual
-natty server daily 20120524 ebs i386 ap-northeast-1 ami-4c00b04d aki-d209a2d3 paravirtual
-natty server daily 20120524 instance-store amd64 ap-northeast-1 ami-3e00b03f aki-d409a2d5 paravirtual
-natty server daily 20120524 instance-store i386 ap-northeast-1 ami-2800b029 aki-d209a2d3 paravirtual
-natty server daily 20120524 ebs amd64 ap-southeast-1 ami-f66325a4 aki-11d5aa43 paravirtual
-natty server daily 20120524 ebs i386 ap-southeast-1 ami-c6632594 aki-13d5aa41 paravirtual
-natty server daily 20120524 instance-store amd64 ap-southeast-1 ami-20632572 aki-11d5aa43 paravirtual
-natty server daily 20120524 instance-store i386 ap-southeast-1 ami-3e63256c aki-13d5aa41 paravirtual
-natty server daily 20120524 ebs amd64 eu-west-1 ami-a5bc86d1 aki-4feec43b paravirtual
-natty server daily 20120524 ebs i386 eu-west-1 ami-b5bc86c1 aki-4deec439 paravirtual
-natty server daily 20120524 instance-store amd64 eu-west-1 ami-c7bc86b3 aki-4feec43b paravirtual
-natty server daily 20120524 instance-store i386 eu-west-1 ami-f9bc868d aki-4deec439 paravirtual
-natty server daily 20120524 ebs amd64 sa-east-1 ami-d4ea34c9 aki-d63ce3cb paravirtual
-natty server daily 20120524 ebs i386 sa-east-1 ami-daea34c7 aki-863ce39b paravirtual
-natty server daily 20120524 instance-store amd64 sa-east-1 ami-deea34c3 aki-d63ce3cb paravirtual
-natty server daily 20120524 instance-store i386 sa-east-1 ami-a0ea34bd aki-863ce39b paravirtual
-natty server daily 20120524 ebs amd64 us-east-1 ami-9ca90ff5 hvm
-natty server daily 20120524 ebs amd64 us-east-1 ami-aea90fc7 aki-427d952b paravirtual
-natty server daily 20120524 ebs i386 us-east-1 ami-faa90f93 aki-407d9529 paravirtual
-natty server daily 20120524 instance-store amd64 us-east-1 ami-9ea80ef7 aki-427d952b paravirtual
-natty server daily 20120524 instance-store i386 us-east-1 ami-e0a80e89 aki-407d9529 paravirtual
-natty server daily 20120524 ebs amd64 us-west-1 ami-f7257fb2 aki-9ba0f1de paravirtual
-natty server daily 20120524 ebs i386 us-west-1 ami-e7257fa2 aki-99a0f1dc paravirtual
-natty server daily 20120524 instance-store amd64 us-west-1 ami-d5257f90 aki-9ba0f1de paravirtual
-natty server daily 20120524 instance-store i386 us-west-1 ami-c5257f80 aki-99a0f1dc paravirtual
-natty server daily 20120524 ebs amd64 us-west-2 ami-f459d5c4 aki-ace26f9c paravirtual
-natty server daily 20120524 ebs i386 us-west-2 ami-f059d5c0 aki-dce26fec paravirtual
-natty server daily 20120524 instance-store amd64 us-west-2 ami-8c59d5bc aki-ace26f9c paravirtual
-natty server daily 20120524 instance-store i386 us-west-2 ami-8659d5b6 aki-dce26fec paravirtual
=== removed file 'environs/ec2/testdata/query/natty/server/released.current.txt'
--- environs/ec2/testdata/query/natty/server/released.current.txt 2012-05-24 16:09:11 +0000
+++ environs/ec2/testdata/query/natty/server/released.current.txt 1970-01-01 00:00:00 +0000
@@ -1,29 +0,0 @@
-natty server release 20120402 ebs amd64 ap-northeast-1 ami-8c3b8a8d aki-d409a2d5 paravirtual
-natty server release 20120402 ebs i386 ap-northeast-1 ami-843b8a85 aki-d209a2d3 paravirtual
-natty server release 20120402 instance-store amd64 ap-northeast-1 ami-5a3b8a5b aki-d409a2d5 paravirtual
-natty server release 20120402 instance-store i386 ap-northeast-1 ami-383b8a39 aki-d209a2d3 paravirtual
-natty server release 20120402 ebs amd64 ap-southeast-1 ami-58692e0a aki-11d5aa43 paravirtual
-natty server release 20120402 ebs i386 ap-southeast-1 ami-5a692e08 aki-13d5aa41 paravirtual
-natty server release 20120402 instance-store amd64 ap-southeast-1 ami-54692e06 aki-11d5aa43 paravirtual
-natty server release 20120402 instance-store i386 ap-southeast-1 ami-b66a2de4 aki-13d5aa41 paravirtual
-natty server release 20120402 ebs amd64 eu-west-1 ami-69b28a1d aki-4feec43b paravirtual
-natty server release 20120402 ebs i386 eu-west-1 ami-6fb28a1b aki-4deec439 paravirtual
-natty server release 20120402 instance-store amd64 eu-west-1 ami-9fb189eb aki-4feec43b paravirtual
-natty server release 20120402 instance-store i386 eu-west-1 ami-95b189e1 aki-4deec439 paravirtual
-natty server release 20120402 ebs amd64 sa-east-1 ami-56b36d4b aki-d63ce3cb paravirtual
-natty server release 20120402 ebs i386 sa-east-1 ami-54b36d49 aki-863ce39b paravirtual
-natty server release 20120402 instance-store amd64 sa-east-1 ami-58b36d45 aki-d63ce3cb paravirtual
-natty server release 20120402 instance-store i386 sa-east-1 ami-2eb36d33 aki-863ce39b paravirtual
-natty server release 20120402 ebs amd64 us-east-1 ami-93c31afa hvm
-natty server release 20120402 ebs amd64 us-east-1 ami-87c31aee aki-427d952b paravirtual
-natty server release 20120402 ebs i386 us-east-1 ami-81c31ae8 aki-407d9529 paravirtual
-natty server release 20120402 instance-store amd64 us-east-1 ami-35c31a5c aki-427d952b paravirtual
-natty server release 20120402 instance-store i386 us-east-1 ami-ffc01996 aki-407d9529 paravirtual
-natty server release 20120402 ebs amd64 us-west-1 ami-cbfda58e aki-9ba0f1de paravirtual
-natty server release 20120402 ebs i386 us-west-1 ami-c9fda58c aki-99a0f1dc paravirtual
-natty server release 20120402 instance-store amd64 us-west-1 ami-33fda576 aki-9ba0f1de paravirtual
-natty server release 20120402 instance-store i386 us-west-1 ami-25fda560 aki-99a0f1dc paravirtual
-natty server release 20120402 ebs amd64 us-west-2 ami-96ef63a6 aki-ace26f9c paravirtual
-natty server release 20120402 ebs i386 us-west-2 ami-92ef63a2 aki-dce26fec paravirtual
-natty server release 20120402 instance-store amd64 us-west-2 ami-beef638e aki-ace26f9c paravirtual
-natty server release 20120402 instance-store i386 us-west-2 ami-b0ef6380 aki-dce26fec paravirtual
=== removed directory 'environs/ec2/testdata/query/oneiric'
=== removed directory 'environs/ec2/testdata/query/oneiric/desktop'
=== removed file 'environs/ec2/testdata/query/oneiric/desktop/daily.current.txt'
--- environs/ec2/testdata/query/oneiric/desktop/daily.current.txt 2012-05-24 16:09:11 +0000
+++ environs/ec2/testdata/query/oneiric/desktop/daily.current.txt 1970-01-01 00:00:00 +0000
@@ -1,29 +0,0 @@
-oneiric desktop daily 20120524 ebs amd64 ap-northeast-1 ami-b802b2b9 aki-ee5df7ef paravirtual
-oneiric desktop daily 20120524 ebs i386 ap-northeast-1 ami-b202b2b3 aki-ec5df7ed paravirtual
-oneiric desktop daily 20120524 instance-store amd64 ap-northeast-1 ami-5802b259 aki-ee5df7ef paravirtual
-oneiric desktop daily 20120524 instance-store i386 ap-northeast-1 ami-2802b229 aki-ec5df7ed paravirtual
-oneiric desktop daily 20120524 ebs amd64 ap-southeast-1 ami-a86026fa aki-aa225af8 paravirtual
-oneiric desktop daily 20120524 ebs i386 ap-southeast-1 ami-bc6026ee aki-a4225af6 paravirtual
-oneiric desktop daily 20120524 instance-store amd64 ap-southeast-1 ami-ee6026bc aki-aa225af8 paravirtual
-oneiric desktop daily 20120524 instance-store i386 ap-southeast-1 ami-f06026a2 aki-a4225af6 paravirtual
-oneiric desktop daily 20120524 ebs amd64 eu-west-1 ami-d5b389a1 aki-62695816 paravirtual
-oneiric desktop daily 20120524 ebs i386 eu-west-1 ami-f7b38983 aki-64695810 paravirtual
-oneiric desktop daily 20120524 instance-store amd64 eu-west-1 ami-79b3890d aki-62695816 paravirtual
-oneiric desktop daily 20120524 instance-store i386 eu-west-1 ami-b5b288c1 aki-64695810 paravirtual
-oneiric desktop daily 20120524 ebs amd64 sa-east-1 ami-38eb3525 aki-cc3ce3d1 paravirtual
-oneiric desktop daily 20120524 ebs i386 sa-east-1 ami-06eb351b aki-bc3ce3a1 paravirtual
-oneiric desktop daily 20120524 instance-store amd64 sa-east-1 ami-10eb350d aki-cc3ce3d1 paravirtual
-oneiric desktop daily 20120524 instance-store i386 sa-east-1 ami-e2ea34ff aki-bc3ce3a1 paravirtual
-oneiric desktop daily 20120524 ebs amd64 us-east-1 ami-76bf191f hvm
-oneiric desktop daily 20120524 ebs amd64 us-east-1 ami-a4be18cd aki-825ea7eb paravirtual
-oneiric desktop daily 20120524 ebs i386 us-east-1 ami-16be187f aki-805ea7e9 paravirtual
-oneiric desktop daily 20120524 instance-store amd64 us-east-1 ami-5ab11733 aki-825ea7eb paravirtual
-oneiric desktop daily 20120524 instance-store i386 us-east-1 ami-0eb01667 aki-805ea7e9 paravirtual
-oneiric desktop daily 20120524 ebs amd64 us-west-1 ami-735b0136 aki-8d396bc8 paravirtual
-oneiric desktop daily 20120524 ebs i386 us-west-1 ami-6b5b012e aki-83396bc6 paravirtual
-oneiric desktop daily 20120524 instance-store amd64 us-west-1 ami-4b5b010e aki-8d396bc8 paravirtual
-oneiric desktop daily 20120524 instance-store i386 us-west-1 ami-a95a00ec aki-83396bc6 paravirtual
-oneiric desktop daily 20120524 ebs amd64 us-west-2 ami-025ad632 aki-98e26fa8 paravirtual
-oneiric desktop daily 20120524 ebs i386 us-west-2 ami-1c5ad62c aki-c2e26ff2 paravirtual
-oneiric desktop daily 20120524 instance-store amd64 us-west-2 ami-2c5ad61c aki-98e26fa8 paravirtual
-oneiric desktop daily 20120524 instance-store i386 us-west-2 ami-205ad610 aki-c2e26ff2 paravirtual
=== removed directory 'environs/ec2/testdata/query/oneiric/server'
=== removed file 'environs/ec2/testdata/query/oneiric/server/daily.current.txt'
--- environs/ec2/testdata/query/oneiric/server/daily.current.txt 2012-05-24 16:09:11 +0000
+++ environs/ec2/testdata/query/oneiric/server/daily.current.txt 1970-01-01 00:00:00 +0000
@@ -1,29 +0,0 @@
-oneiric server daily 20120523 ebs amd64 ap-northeast-1 ami-7419a975 aki-ee5df7ef paravirtual
-oneiric server daily 20120523 ebs i386 ap-northeast-1 ami-7019a971 aki-ec5df7ed paravirtual
-oneiric server daily 20120523 instance-store amd64 ap-northeast-1 ami-3c19a93d aki-ee5df7ef paravirtual
-oneiric server daily 20120523 instance-store i386 ap-northeast-1 ami-2219a923 aki-ec5df7ed paravirtual
-oneiric server daily 20120523 ebs amd64 ap-southeast-1 ami-d866208a aki-aa225af8 paravirtual
-oneiric server daily 20120523 ebs i386 ap-southeast-1 ami-2866207a aki-a4225af6 paravirtual
-oneiric server daily 20120523 instance-store amd64 ap-southeast-1 ami-02662050 aki-aa225af8 paravirtual
-oneiric server daily 20120523 instance-store i386 ap-southeast-1 ami-64662036 aki-a4225af6 paravirtual
-oneiric server daily 20120523 ebs amd64 eu-west-1 ami-3dc7fd49 aki-62695816 paravirtual
-oneiric server daily 20120523 ebs i386 eu-west-1 ami-5bc7fd2f aki-64695810 paravirtual
-oneiric server daily 20120523 instance-store amd64 eu-west-1 ami-9bc6fcef aki-62695816 paravirtual
-oneiric server daily 20120523 instance-store i386 eu-west-1 ami-cbc6fcbf aki-64695810 paravirtual
-oneiric server daily 20120523 ebs amd64 sa-east-1 ami-2ced3331 aki-cc3ce3d1 paravirtual
-oneiric server daily 20120523 ebs i386 sa-east-1 ami-30ed332d aki-bc3ce3a1 paravirtual
-oneiric server daily 20120523 instance-store amd64 sa-east-1 ami-3ced3321 aki-cc3ce3d1 paravirtual
-oneiric server daily 20120523 instance-store i386 sa-east-1 ami-06ed331b aki-bc3ce3a1 paravirtual
-oneiric server daily 20120523 ebs amd64 us-east-1 ami-22ce684b hvm
-oneiric server daily 20120523 ebs amd64 us-east-1 ami-6cce6805 aki-825ea7eb paravirtual
-oneiric server daily 20120523 ebs i386 us-east-1 ami-aac167c3 aki-805ea7e9 paravirtual
-oneiric server daily 20120523 instance-store amd64 us-east-1 ami-c2c066ab aki-825ea7eb paravirtual
-oneiric server daily 20120523 instance-store i386 us-east-1 ami-38c06651 aki-805ea7e9 paravirtual
-oneiric server daily 20120523 ebs amd64 us-west-1 ami-812278c4 aki-8d396bc8 paravirtual
-oneiric server daily 20120523 ebs i386 us-west-1 ami-eb2278ae aki-83396bc6 paravirtual
-oneiric server daily 20120523 instance-store amd64 us-west-1 ami-c1227884 aki-8d396bc8 paravirtual
-oneiric server daily 20120523 instance-store i386 us-west-1 ami-35227870 aki-83396bc6 paravirtual
-oneiric server daily 20120523 ebs amd64 us-west-2 ami-1c5fd32c aki-98e26fa8 paravirtual
-oneiric server daily 20120523 ebs i386 us-west-2 ami-185fd328 aki-c2e26ff2 paravirtual
-oneiric server daily 20120523 instance-store amd64 us-west-2 ami-2c5fd31c aki-98e26fa8 paravirtual
-oneiric server daily 20120523 instance-store i386 us-west-2 ami-225fd312 aki-c2e26ff2 paravirtual
=== removed file 'environs/ec2/testdata/query/oneiric/server/released.current.txt'
--- environs/ec2/testdata/query/oneiric/server/released.current.txt 2012-05-24 16:09:11 +0000
+++ environs/ec2/testdata/query/oneiric/server/released.current.txt 1970-01-01 00:00:00 +0000
@@ -1,29 +0,0 @@
-oneiric server release 20120401 ebs amd64 ap-northeast-1 ami-44328345 aki-ee5df7ef paravirtual
-oneiric server release 20120401 ebs i386 ap-northeast-1 ami-42328343 aki-ec5df7ed paravirtual
-oneiric server release 20120401 instance-store amd64 ap-northeast-1 ami-40328341 aki-ee5df7ef paravirtual
-oneiric server release 20120401 instance-store i386 ap-northeast-1 ami-2c32832d aki-ec5df7ed paravirtual
-oneiric server release 20120401 ebs amd64 ap-southeast-1 ami-8e6027dc aki-aa225af8 paravirtual
-oneiric server release 20120401 ebs i386 ap-southeast-1 ami-886027da aki-a4225af6 paravirtual
-oneiric server release 20120401 instance-store amd64 ap-southeast-1 ami-8a6027d8 aki-aa225af8 paravirtual
-oneiric server release 20120401 instance-store i386 ap-southeast-1 ami-9c6027ce aki-a4225af6 paravirtual
-oneiric server release 20120401 ebs amd64 eu-west-1 ami-81c5fdf5 aki-62695816 paravirtual
-oneiric server release 20120401 ebs i386 eu-west-1 ami-87c5fdf3 aki-64695810 paravirtual
-oneiric server release 20120401 instance-store amd64 eu-west-1 ami-a1c5fdd5 aki-62695816 paravirtual
-oneiric server release 20120401 instance-store i386 eu-west-1 ami-d5c5fda1 aki-64695810 paravirtual
-oneiric server release 20120401 ebs amd64 sa-east-1 ami-30b56b2d aki-cc3ce3d1 paravirtual
-oneiric server release 20120401 ebs i386 sa-east-1 ami-36b56b2b aki-bc3ce3a1 paravirtual
-oneiric server release 20120401 instance-store amd64 sa-east-1 ami-34b56b29 aki-cc3ce3d1 paravirtual
-oneiric server release 20120401 instance-store i386 sa-east-1 ami-0eb56b13 aki-bc3ce3a1 paravirtual
-oneiric server release 20120401 ebs amd64 us-east-1 ami-4fad7426 hvm
-oneiric server release 20120401 ebs amd64 us-east-1 ami-4dad7424 aki-825ea7eb paravirtual
-oneiric server release 20120401 ebs i386 us-east-1 ami-4bad7422 aki-805ea7e9 paravirtual
-oneiric server release 20120401 instance-store amd64 us-east-1 ami-8baa73e2 aki-825ea7eb paravirtual
-oneiric server release 20120401 instance-store i386 us-east-1 ami-e1aa7388 aki-805ea7e9 paravirtual
-oneiric server release 20120401 ebs amd64 us-west-1 ami-11c59d54 aki-8d396bc8 paravirtual
-oneiric server release 20120401 ebs i386 us-west-1 ami-17c59d52 aki-83396bc6 paravirtual
-oneiric server release 20120401 instance-store amd64 us-west-1 ami-0fc59d4a aki-8d396bc8 paravirtual
-oneiric server release 20120401 instance-store i386 us-west-1 ami-79c59d3c aki-83396bc6 paravirtual
-oneiric server release 20120401 ebs amd64 us-west-2 ami-a8ec6098 aki-98e26fa8 paravirtual
-oneiric server release 20120401 ebs i386 us-west-2 ami-a6ec6096 aki-c2e26ff2 paravirtual
-oneiric server release 20120401 instance-store amd64 us-west-2 ami-a4ec6094 aki-98e26fa8 paravirtual
-oneiric server release 20120401 instance-store i386 us-west-2 ami-b2ec6082 aki-c2e26ff2 paravirtual
=== removed directory 'environs/ec2/testdata/query/precise'
=== removed directory 'environs/ec2/testdata/query/precise/desktop'
=== removed file 'environs/ec2/testdata/query/precise/desktop/daily.current.txt'
--- environs/ec2/testdata/query/precise/desktop/daily.current.txt 2012-05-24 16:05:28 +0000
+++ environs/ec2/testdata/query/precise/desktop/daily.current.txt 1970-01-01 00:00:00 +0000
@@ -1,29 +0,0 @@
-precise desktop daily 20120516 ebs amd64 ap-northeast-1 ami-084bfb09 aki-ee5df7ef paravirtual
-precise desktop daily 20120516 ebs i386 ap-northeast-1 ami-ee4afaef aki-ec5df7ed paravirtual
-precise desktop daily 20120516 instance-store amd64 ap-northeast-1 ami-d64afad7 aki-ee5df7ef paravirtual
-precise desktop daily 20120516 instance-store i386 ap-northeast-1 ami-924afa93 aki-ec5df7ed paravirtual
-precise desktop daily 20120516 ebs amd64 ap-southeast-1 ami-40387e12 aki-aa225af8 paravirtual
-precise desktop daily 20120516 ebs i386 ap-southeast-1 ami-a03b7df2 aki-a4225af6 paravirtual
-precise desktop daily 20120516 instance-store amd64 ap-southeast-1 ami-8e3b7ddc aki-aa225af8 paravirtual
-precise desktop daily 20120516 instance-store i386 ap-southeast-1 ami-e63b7db4 aki-a4225af6 paravirtual
-precise desktop daily 20120516 ebs amd64 eu-west-1 ami-15033961 aki-62695816 paravirtual
-precise desktop daily 20120516 ebs i386 eu-west-1 ami-3d033949 aki-64695810 paravirtual
-precise desktop daily 20120516 instance-store amd64 eu-west-1 ami-af0238db aki-62695816 paravirtual
-precise desktop daily 20120516 instance-store i386 eu-west-1 ami-c50238b1 aki-64695810 paravirtual
-precise desktop daily 20120516 ebs amd64 sa-east-1 ami-60f7297d aki-cc3ce3d1 paravirtual
-precise desktop daily 20120516 ebs i386 sa-east-1 ami-6af72977 aki-bc3ce3a1 paravirtual
-precise desktop daily 20120516 instance-store amd64 sa-east-1 ami-74f72969 aki-cc3ce3d1 paravirtual
-precise desktop daily 20120516 instance-store i386 sa-east-1 ami-78f72965 aki-bc3ce3a1 paravirtual
-precise desktop daily 20120516 ebs amd64 us-east-1 ami-a8c265c1 hvm
-precise desktop daily 20120516 ebs amd64 us-east-1 ami-e8c26581 aki-825ea7eb paravirtual
-precise desktop daily 20120516 ebs i386 us-east-1 ami-72c2651b aki-805ea7e9 paravirtual
-precise desktop daily 20120516 instance-store amd64 us-east-1 ami-34c3645d aki-825ea7eb paravirtual
-precise desktop daily 20120516 instance-store i386 us-east-1 ami-d4c463bd aki-805ea7e9 paravirtual
-precise desktop daily 20120516 ebs amd64 us-west-1 ami-4f1d470a aki-8d396bc8 paravirtual
-precise desktop daily 20120516 ebs i386 us-west-1 ami-b11c46f4 aki-83396bc6 paravirtual
-precise desktop daily 20120516 instance-store amd64 us-west-1 ami-ff1c46ba aki-8d396bc8 paravirtual
-precise desktop daily 20120516 instance-store i386 us-west-1 ami-e71c46a2 aki-83396bc6 paravirtual
-precise desktop daily 20120516 ebs amd64 us-west-2 ami-044fc334 aki-98e26fa8 paravirtual
-precise desktop daily 20120516 ebs i386 us-west-2 ami-164fc326 aki-c2e26ff2 paravirtual
-precise desktop daily 20120516 instance-store amd64 us-west-2 ami-3c4fc30c aki-98e26fa8 paravirtual
-precise desktop daily 20120516 instance-store i386 us-west-2 ami-3a4fc30a aki-c2e26ff2 paravirtual
=== removed directory 'environs/ec2/testdata/query/precise/server'
=== removed file 'environs/ec2/testdata/query/precise/server/daily.current.txt'
--- environs/ec2/testdata/query/precise/server/daily.current.txt 2012-05-24 16:05:28 +0000
+++ environs/ec2/testdata/query/precise/server/daily.current.txt 1970-01-01 00:00:00 +0000
@@ -1,29 +0,0 @@
-precise server daily 20120519 ebs amd64 ap-northeast-1 ami-5a3e8e5b aki-ee5df7ef paravirtual
-precise server daily 20120519 ebs i386 ap-northeast-1 ami-523e8e53 aki-ec5df7ed paravirtual
-precise server daily 20120519 instance-store amd64 ap-northeast-1 ami-3c3e8e3d aki-ee5df7ef paravirtual
-precise server daily 20120519 instance-store i386 ap-northeast-1 ami-3a3e8e3b aki-ec5df7ed paravirtual
-precise server daily 20120519 ebs amd64 ap-southeast-1 ami-484a0c1a aki-aa225af8 paravirtual
-precise server daily 20120519 ebs i386 ap-southeast-1 ami-444a0c16 aki-a4225af6 paravirtual
-precise server daily 20120519 instance-store amd64 ap-southeast-1 ami-a84d0bfa aki-aa225af8 paravirtual
-precise server daily 20120519 instance-store i386 ap-southeast-1 ami-a64d0bf4 aki-a4225af6 paravirtual
-precise server daily 20120519 ebs amd64 eu-west-1 ami-abefd5df aki-62695816 paravirtual
-precise server daily 20120519 ebs i386 eu-west-1 ami-bdefd5c9 aki-64695810 paravirtual
-precise server daily 20120519 instance-store amd64 eu-west-1 ami-cbefd5bf aki-62695816 paravirtual
-precise server daily 20120519 instance-store i386 eu-west-1 ami-cfefd5bb aki-64695810 paravirtual
-precise server daily 20120519 ebs amd64 sa-east-1 ami-64f32d79 aki-cc3ce3d1 paravirtual
-precise server daily 20120519 ebs i386 sa-east-1 ami-6af32d77 aki-bc3ce3a1 paravirtual
-precise server daily 20120519 instance-store amd64 sa-east-1 ami-6ef32d73 aki-cc3ce3d1 paravirtual
-precise server daily 20120519 instance-store i386 sa-east-1 ami-6cf32d71 aki-bc3ce3a1 paravirtual
-precise server daily 20120519 ebs amd64 us-east-1 ami-547fd83d hvm
-precise server daily 20120519 ebs amd64 us-east-1 ami-c670d7af aki-825ea7eb paravirtual
-precise server daily 20120519 ebs i386 us-east-1 ami-1470d77d aki-805ea7e9 paravirtual
-precise server daily 20120519 instance-store amd64 us-east-1 ami-4c70d725 aki-825ea7eb paravirtual
-precise server daily 20120519 instance-store i386 us-east-1 ami-bc71d6d5 aki-805ea7e9 paravirtual
-precise server daily 20120519 ebs amd64 us-west-1 ami-45065c00 aki-8d396bc8 paravirtual
-precise server daily 20120519 ebs i386 us-west-1 ami-bd015bf8 aki-83396bc6 paravirtual
-precise server daily 20120519 instance-store amd64 us-west-1 ami-a3015be6 aki-8d396bc8 paravirtual
-precise server daily 20120519 instance-store i386 us-west-1 ami-91015bd4 aki-83396bc6 paravirtual
-precise server daily 20120519 ebs amd64 us-west-2 ami-3a46ca0a aki-98e26fa8 paravirtual
-precise server daily 20120519 ebs i386 us-west-2 ami-3646ca06 aki-c2e26ff2 paravirtual
-precise server daily 20120519 instance-store amd64 us-west-2 ami-3046ca00 aki-98e26fa8 paravirtual
-precise server daily 20120519 instance-store i386 us-west-2 ami-ce45c9fe aki-c2e26ff2 paravirtual
=== removed file 'environs/ec2/testdata/query/precise/server/released.current.txt'
--- environs/ec2/testdata/query/precise/server/released.current.txt 2012-06-28 23:52:38 +0000
+++ environs/ec2/testdata/query/precise/server/released.current.txt 1970-01-01 00:00:00 +0000
@@ -1,33 +0,0 @@
-precise server release 20120424 ebs amd64 ap-northeast-1 ami-60c77761 aki-ee5df7ef paravirtual
-precise server release 20120424 ebs i386 ap-northeast-1 ami-5ec7775f aki-ec5df7ed paravirtual
-precise server release 20120424 instance-store amd64 ap-northeast-1 ami-2cc7772d aki-ee5df7ef paravirtual
-precise server release 20120424 instance-store i386 ap-northeast-1 ami-12c77713 aki-ec5df7ed paravirtual
-precise server release 20120424 ebs amd64 ap-southeast-1 ami-a4ca8df6 aki-aa225af8 paravirtual
-precise server release 20120424 ebs i386 ap-southeast-1 ami-a6ca8df4 aki-a4225af6 paravirtual
-precise server release 20120424 instance-store amd64 ap-southeast-1 ami-a0ca8df2 aki-aa225af8 paravirtual
-precise server release 20120424 instance-store i386 ap-southeast-1 ami-88ca8dda aki-a4225af6 paravirtual
-precise server release 20120424 ebs amd64 eu-west-1 ami-e1e8d395 aki-62695816 paravirtual
-precise server release 20120424 ebs i386 eu-west-1 ami-e7e8d393 aki-64695810 paravirtual
-precise server release 20120424 instance-store amd64 eu-west-1 ami-1de8d369 aki-62695816 paravirtual
-precise server release 20120424 instance-store i386 eu-west-1 ami-25e8d351 aki-64695810 paravirtual
-precise server release 20120424 ebs amd64 sa-east-1 ami-8cd80691 aki-cc3ce3d1 paravirtual
-precise server release 20120424 ebs i386 sa-east-1 ami-92d8068f aki-bc3ce3a1 paravirtual
-precise server release 20120424 instance-store amd64 sa-east-1 ami-96d8068b aki-cc3ce3d1 paravirtual
-precise server release 20120424 instance-store i386 sa-east-1 ami-9cd80681 aki-bc3ce3a1 paravirtual
-precise server release 20120424 ebs amd64 us-east-1 ami-a69943cf hvm
-precise server release 20120424 ebs amd64 us-east-1 ami-a29943cb aki-825ea7eb paravirtual
-precise server release 20120424 ebs i386 us-east-1 ami-ac9943c5 aki-805ea7e9 paravirtual
-precise server release 20120424 instance-store amd64 us-east-1 ami-3c994355 aki-825ea7eb paravirtual
-precise server release 20120424 instance-store i386 us-east-1 ami-b89842d1 aki-805ea7e9 paravirtual
-precise server release 20120424 ebs amd64 us-west-1 ami-87712ac2 aki-8d396bc8 paravirtual
-precise server release 20120424 ebs i386 us-west-1 ami-85712ac0 aki-83396bc6 paravirtual
-precise server release 20120424 instance-store amd64 us-west-1 ami-e7712aa2 aki-8d396bc8 paravirtual
-precise server release 20120424 instance-store i386 us-west-1 ami-d5712a90 aki-83396bc6 paravirtual
-precise server release 20120424 ebs amd64 us-west-2 ami-20800c10 aki-98e26fa8 paravirtual
-precise server release 20120424 ebs i386 us-west-2 ami-3e800c0e aki-c2e26ff2 paravirtual
-precise server release 20120424 instance-store amd64 us-west-2 ami-38800c08 aki-98e26fa8 paravirtual
-precise server release 20120424 instance-store i386 us-west-2 ami-cc870bfc aki-c2e26ff2 paravirtual
-precise server release 20120424 ebs amd64 test ami-20800c10 aki-98e26fa8 paravirtual
-precise server release 20120424 ebs i386 test ami-3e800c0e aki-c2e26ff2 paravirtual
-precise server release 20120424 instance-store amd64 test ami-38800c08 aki-98e26fa8 paravirtual
-precise server release 20120424 instance-store i386 test ami-cc870bfc aki-c2e26ff2 paravirtual
=== removed directory 'environs/ec2/testdata/query/quantal'
=== removed directory 'environs/ec2/testdata/query/quantal/desktop'
=== removed file 'environs/ec2/testdata/query/quantal/desktop/daily.current.txt'
--- environs/ec2/testdata/query/quantal/desktop/daily.current.txt 2012-05-24 16:05:28 +0000
+++ environs/ec2/testdata/query/quantal/desktop/daily.current.txt 1970-01-01 00:00:00 +0000
@@ -1,29 +0,0 @@
-quantal desktop daily 20120518 ebs amd64 ap-northeast-1 ami-d83b8bd9 aki-ee5df7ef paravirtual
-quantal desktop daily 20120518 ebs i386 ap-northeast-1 ami-823b8b83 aki-ec5df7ed paravirtual
-quantal desktop daily 20120518 instance-store amd64 ap-northeast-1 ami-4a3b8b4b aki-ee5df7ef paravirtual
-quantal desktop daily 20120518 instance-store i386 ap-northeast-1 ami-323b8b33 aki-ec5df7ed paravirtual
-quantal desktop daily 20120518 ebs amd64 ap-southeast-1 ami-0a4f0958 aki-aa225af8 paravirtual
-quantal desktop daily 20120518 ebs i386 ap-southeast-1 ami-184f094a aki-a4225af6 paravirtual
-quantal desktop daily 20120518 instance-store amd64 ap-southeast-1 ami-4c4f091e aki-aa225af8 paravirtual
-quantal desktop daily 20120518 instance-store i386 ap-southeast-1 ami-5a4f0908 aki-a4225af6 paravirtual
-quantal desktop daily 20120518 ebs amd64 eu-west-1 ami-3dead049 aki-62695816 paravirtual
-quantal desktop daily 20120518 ebs i386 eu-west-1 ami-69ead01d aki-64695810 paravirtual
-quantal desktop daily 20120518 instance-store amd64 eu-west-1 ami-c9f5cfbd aki-62695816 paravirtual
-quantal desktop daily 20120518 instance-store i386 eu-west-1 ami-eff5cf9b aki-64695810 paravirtual
-quantal desktop daily 20120518 ebs amd64 sa-east-1 ami-82f22c9f aki-cc3ce3d1 paravirtual
-quantal desktop daily 20120518 ebs i386 sa-east-1 ami-86f22c9b aki-bc3ce3a1 paravirtual
-quantal desktop daily 20120518 instance-store amd64 sa-east-1 ami-90f22c8d aki-cc3ce3d1 paravirtual
-quantal desktop daily 20120518 instance-store i386 sa-east-1 ami-9ef22c83 aki-bc3ce3a1 paravirtual
-quantal desktop daily 20120518 ebs amd64 us-east-1 ami-3a67c053 hvm
-quantal desktop daily 20120518 ebs amd64 us-east-1 ami-84983fed aki-825ea7eb paravirtual
-quantal desktop daily 20120518 ebs i386 us-east-1 ami-ea983f83 aki-805ea7e9 paravirtual
-quantal desktop daily 20120518 instance-store amd64 us-east-1 ami-ca993ea3 aki-825ea7eb paravirtual
-quantal desktop daily 20120518 instance-store i386 us-east-1 ami-6a993e03 aki-805ea7e9 paravirtual
-quantal desktop daily 20120518 ebs amd64 us-west-1 ami-290d576c aki-8d396bc8 paravirtual
-quantal desktop daily 20120518 ebs i386 us-west-1 ami-1f0d575a aki-83396bc6 paravirtual
-quantal desktop daily 20120518 instance-store amd64 us-west-1 ami-650d5720 aki-8d396bc8 paravirtual
-quantal desktop daily 20120518 instance-store i386 us-west-1 ami-490d570c aki-83396bc6 paravirtual
-quantal desktop daily 20120518 ebs amd64 us-west-2 ami-f844c8c8 aki-98e26fa8 paravirtual
-quantal desktop daily 20120518 ebs i386 us-west-2 ami-f044c8c0 aki-c2e26ff2 paravirtual
-quantal desktop daily 20120518 instance-store amd64 us-west-2 ami-8444c8b4 aki-98e26fa8 paravirtual
-quantal desktop daily 20120518 instance-store i386 us-west-2 ami-9844c8a8 aki-c2e26ff2 paravirtual
=== removed directory 'environs/ec2/testdata/query/quantal/server'
=== removed file 'environs/ec2/testdata/query/quantal/server/daily.current.txt'
--- environs/ec2/testdata/query/quantal/server/daily.current.txt 2012-11-26 23:34:28 +0000
+++ environs/ec2/testdata/query/quantal/server/daily.current.txt 1970-01-01 00:00:00 +0000
@@ -1,35 +0,0 @@
-quantal server daily 20121118 ebs amd64 ap-northeast-1 ami-426ad343 aki-ee5df7ef paravirtual
-quantal server daily 20121118 ebs i386 ap-northeast-1 ami-3c6ad33d aki-ec5df7ed paravirtual
-quantal server daily 20121118 instance-store amd64 ap-northeast-1 ami-006ad301 aki-ee5df7ef paravirtual
-quantal server daily 20121118 instance-store i386 ap-northeast-1 ami-d46bd2d5 aki-ec5df7ed paravirtual
-quantal server daily 20121118 ebs amd64 ap-southeast-1 ami-be7536ec aki-aa225af8 paravirtual
-quantal server daily 20121118 ebs i386 ap-southeast-1 ami-ba7536e8 aki-a4225af6 paravirtual
-quantal server daily 20121118 instance-store amd64 ap-southeast-1 ami-847536d6 aki-aa225af8 paravirtual
-quantal server daily 20121118 instance-store i386 ap-southeast-1 ami-e67536b4 aki-a4225af6 paravirtual
-quantal server daily 20121118 ebs amd64 ap-southeast-2 ami-2fb52215 aki-31990e0b paravirtual
-quantal server daily 20121118 ebs i386 ap-southeast-2 ami-29b52213 aki-33990e09 paravirtual
-quantal server daily 20121118 instance-store amd64 ap-southeast-2 ami-37b5220d aki-31990e0b paravirtual
-quantal server daily 20121118 instance-store i386 ap-southeast-2 ami-c3b621f9 aki-33990e09 paravirtual
-quantal server daily 20121118 ebs amd64 eu-west-1 ami-75393b01 hvm
-quantal server daily 20121118 ebs amd64 eu-west-1 ami-87383af3 aki-62695816 paravirtual
-quantal server daily 20121118 ebs i386 eu-west-1 ami-99383aed aki-64695810 paravirtual
-quantal server daily 20121118 instance-store amd64 eu-west-1 ami-ef383a9b aki-62695816 paravirtual
-quantal server daily 20121118 instance-store i386 eu-west-1 ami-2b383a5f aki-64695810 paravirtual
-quantal server daily 20121118 ebs amd64 sa-east-1 ami-b6944cab aki-cc3ce3d1 paravirtual
-quantal server daily 20121118 ebs i386 sa-east-1 ami-b4944ca9 aki-bc3ce3a1 paravirtual
-quantal server daily 20121118 instance-store amd64 sa-east-1 ami-88944c95 aki-cc3ce3d1 paravirtual
-quantal server daily 20121118 instance-store i386 sa-east-1 ami-9c944c81 aki-bc3ce3a1 paravirtual
-quantal server daily 20121118 ebs amd64 us-east-1 ami-c35cdbaa hvm
-quantal server daily 20121118 ebs amd64 us-east-1 ami-e55cdb8c aki-825ea7eb paravirtual
-quantal server daily 20121118 ebs i386 us-east-1 ami-0f5cdb66 aki-805ea7e9 paravirtual
-quantal server daily 20121118 instance-store amd64 us-east-1 ami-1b5ed972 aki-825ea7eb paravirtual
-quantal server daily 20121118 instance-store i386 us-east-1 ami-bb51d6d2 aki-805ea7e9 paravirtual
-quantal server daily 20121118 ebs amd64 us-west-1 ami-3c664779 aki-8d396bc8 paravirtual
-quantal server daily 20121118 ebs i386 us-west-1 ami-30664775 aki-83396bc6 paravirtual
-quantal server daily 20121118 instance-store amd64 us-west-1 ami-7c664739 aki-8d396bc8 paravirtual
-quantal server daily 20121118 instance-store i386 us-west-1 ami-a86746ed aki-83396bc6 paravirtual
-quantal server daily 20121118 ebs amd64 us-west-2 ami-06c24a36 hvm
-quantal server daily 20121118 ebs amd64 us-west-2 ami-16c24a26 aki-98e26fa8 paravirtual
-quantal server daily 20121118 ebs i386 us-west-2 ami-20c24a10 aki-c2e26ff2 paravirtual
-quantal server daily 20121118 instance-store amd64 us-west-2 ami-d8c149e8 aki-98e26fa8 paravirtual
-quantal server daily 20121118 instance-store i386 us-west-2 ami-eec048de aki-c2e26ff2 paravirtual
=== removed file 'environs/ec2/testdata/query/quantal/server/released.current.txt'
--- environs/ec2/testdata/query/quantal/server/released.current.txt 2012-11-26 23:34:28 +0000
+++ environs/ec2/testdata/query/quantal/server/released.current.txt 1970-01-01 00:00:00 +0000
@@ -1,39 +0,0 @@
-quantal server release 20121017 ebs amd64 ap-northeast-1 ami-3c07b83d aki-ee5df7ef paravirtual
-quantal server release 20121017 ebs i386 ap-northeast-1 ami-3a07b83b aki-ec5df7ed paravirtual
-quantal server release 20121017 instance-store amd64 ap-northeast-1 ami-0407b805 aki-ee5df7ef paravirtual
-quantal server release 20121017 instance-store i386 ap-northeast-1 ami-7008b771 aki-ec5df7ed paravirtual
-quantal server release 20121017 ebs amd64 ap-southeast-1 ami-8ed393dc aki-aa225af8 paravirtual
-quantal server release 20121017 ebs i386 ap-southeast-1 ami-88d393da aki-a4225af6 paravirtual
-quantal server release 20121017 instance-store amd64 ap-southeast-1 ami-e6d393b4 aki-aa225af8 paravirtual
-quantal server release 20121017 instance-store i386 ap-southeast-1 ami-28d3937a aki-a4225af6 paravirtual
-quantal server release 20121017 ebs amd64 ap-southeast-2 ami-df8611e5 aki-31990e0b paravirtual
-quantal server release 20121017 ebs i386 ap-southeast-2 ami-d98611e3 aki-33990e09 paravirtual
-quantal server release 20121017 instance-store amd64 ap-southeast-2 ami-db8611e1 aki-31990e0b paravirtual
-quantal server release 20121017 instance-store i386 ap-southeast-2 ami-e38611d9 aki-33990e09 paravirtual
-quantal server release 20121017 ebs amd64 eu-west-1 ami-ebeded9f hvm
-quantal server release 20121017 ebs amd64 eu-west-1 ami-e9eded9d aki-62695816 paravirtual
-quantal server release 20121017 ebs i386 eu-west-1 ami-efeded9b aki-64695810 paravirtual
-quantal server release 20121017 instance-store amd64 eu-west-1 ami-69eded1d aki-62695816 paravirtual
-quantal server release 20121017 instance-store i386 eu-west-1 ami-b5ececc1 aki-64695810 paravirtual
-quantal server release 20121017 ebs amd64 sa-east-1 ami-cc30e9d1 aki-cc3ce3d1 paravirtual
-quantal server release 20121017 ebs i386 sa-east-1 ami-d230e9cf aki-bc3ce3a1 paravirtual
-quantal server release 20121017 instance-store amd64 sa-east-1 ami-d030e9cd aki-cc3ce3d1 paravirtual
-quantal server release 20121017 instance-store i386 sa-east-1 ami-b230e9af aki-bc3ce3a1 paravirtual
-quantal server release 20121017 ebs amd64 us-east-1 ami-9665dbff hvm
-quantal server release 20121017 ebs amd64 us-east-1 ami-9465dbfd aki-825ea7eb paravirtual
-quantal server release 20121017 ebs i386 us-east-1 ami-9265dbfb aki-805ea7e9 paravirtual
-quantal server release 20121017 instance-store amd64 us-east-1 ami-e864da81 aki-825ea7eb paravirtual
-quantal server release 20121017 instance-store i386 us-east-1 ami-dc66d8b5 aki-805ea7e9 paravirtual
-quantal server release 20121017 ebs amd64 us-west-1 ami-8f83a4ca aki-8d396bc8 paravirtual
-quantal server release 20121017 ebs i386 us-west-1 ami-8d83a4c8 aki-83396bc6 paravirtual
-quantal server release 20121017 instance-store amd64 us-west-1 ami-fd83a4b8 aki-8d396bc8 paravirtual
-quantal server release 20121017 instance-store i386 us-west-1 ami-0983a44c aki-83396bc6 paravirtual
-quantal server release 20121017 ebs amd64 us-west-2 ami-42f97072 hvm
-quantal server release 20121017 ebs amd64 us-west-2 ami-40f97070 aki-98e26fa8 paravirtual
-quantal server release 20121017 ebs i386 us-west-2 ami-5ef9706e aki-c2e26ff2 paravirtual
-quantal server release 20121017 instance-store amd64 us-west-2 ami-50f97060 aki-98e26fa8 paravirtual
-quantal server release 20121017 instance-store i386 us-west-2 ami-00f97030 aki-c2e26ff2 paravirtual
-quantal server release 20121017 ebs amd64 test ami-40f97070 aki-98e26fa8 paravirtual
-quantal server release 20121017 ebs i386 test ami-5ef9706e aki-c2e26ff2 paravirtual
-quantal server release 20121017 instance-store amd64 test ami-50f97060 aki-98e26fa8 paravirtual
-quantal server release 20121017 instance-store i386 test ami-00f97030 aki-c2e26ff2 paravirtual
=== removed directory 'environs/ec2/testdata/query/raring'
=== removed directory 'environs/ec2/testdata/query/raring/server'
=== removed file 'environs/ec2/testdata/query/raring/server/released.current.txt'
--- environs/ec2/testdata/query/raring/server/released.current.txt 2013-02-25 00:47:09 +0000
+++ environs/ec2/testdata/query/raring/server/released.current.txt 1970-01-01 00:00:00 +0000
@@ -1,39 +0,0 @@
-raring server release 20121017 ebs amd64 ap-northeast-1 ami-3c07b83d aki-ee5df7ef paravirtual
-raring server release 20121017 ebs i386 ap-northeast-1 ami-3a07b83b aki-ec5df7ed paravirtual
-raring server release 20121017 instance-store amd64 ap-northeast-1 ami-0407b805 aki-ee5df7ef paravirtual
-raring server release 20121017 instance-store i386 ap-northeast-1 ami-7008b771 aki-ec5df7ed paravirtual
-raring server release 20121017 ebs amd64 ap-southeast-1 ami-8ed393dc aki-aa225af8 paravirtual
-raring server release 20121017 ebs i386 ap-southeast-1 ami-88d393da aki-a4225af6 paravirtual
-raring server release 20121017 instance-store amd64 ap-southeast-1 ami-e6d393b4 aki-aa225af8 paravirtual
-raring server release 20121017 instance-store i386 ap-southeast-1 ami-28d3937a aki-a4225af6 paravirtual
-raring server release 20121017 ebs amd64 ap-southeast-2 ami-df8611e5 aki-31990e0b paravirtual
-raring server release 20121017 ebs i386 ap-southeast-2 ami-d98611e3 aki-33990e09 paravirtual
-raring server release 20121017 instance-store amd64 ap-southeast-2 ami-db8611e1 aki-31990e0b paravirtual
-raring server release 20121017 instance-store i386 ap-southeast-2 ami-e38611d9 aki-33990e09 paravirtual
-raring server release 20121017 ebs amd64 eu-west-1 ami-ebeded9f hvm
-raring server release 20121017 ebs amd64 eu-west-1 ami-e9eded9d aki-62695816 paravirtual
-raring server release 20121017 ebs i386 eu-west-1 ami-efeded9b aki-64695810 paravirtual
-raring server release 20121017 instance-store amd64 eu-west-1 ami-69eded1d aki-62695816 paravirtual
-raring server release 20121017 instance-store i386 eu-west-1 ami-b5ececc1 aki-64695810 paravirtual
-raring server release 20121017 ebs amd64 sa-east-1 ami-cc30e9d1 aki-cc3ce3d1 paravirtual
-raring server release 20121017 ebs i386 sa-east-1 ami-d230e9cf aki-bc3ce3a1 paravirtual
-raring server release 20121017 instance-store amd64 sa-east-1 ami-d030e9cd aki-cc3ce3d1 paravirtual
-raring server release 20121017 instance-store i386 sa-east-1 ami-b230e9af aki-bc3ce3a1 paravirtual
-raring server release 20121017 ebs amd64 us-east-1 ami-9665dbff hvm
-raring server release 20121017 ebs amd64 us-east-1 ami-9465dbfd aki-825ea7eb paravirtual
-raring server release 20121017 ebs i386 us-east-1 ami-9265dbfb aki-805ea7e9 paravirtual
-raring server release 20121017 instance-store amd64 us-east-1 ami-e864da81 aki-825ea7eb paravirtual
-raring server release 20121017 instance-store i386 us-east-1 ami-dc66d8b5 aki-805ea7e9 paravirtual
-raring server release 20121017 ebs amd64 us-west-1 ami-8f83a4ca aki-8d396bc8 paravirtual
-raring server release 20121017 ebs i386 us-west-1 ami-8d83a4c8 aki-83396bc6 paravirtual
-raring server release 20121017 instance-store amd64 us-west-1 ami-fd83a4b8 aki-8d396bc8 paravirtual
-raring server release 20121017 instance-store i386 us-west-1 ami-0983a44c aki-83396bc6 paravirtual
-raring server release 20121017 ebs amd64 us-west-2 ami-42f97072 hvm
-raring server release 20121017 ebs amd64 us-west-2 ami-40f97070 aki-98e26fa8 paravirtual
-raring server release 20121017 ebs i386 us-west-2 ami-5ef9706e aki-c2e26ff2 paravirtual
-raring server release 20121017 instance-store amd64 us-west-2 ami-50f97060 aki-98e26fa8 paravirtual
-raring server release 20121017 instance-store i386 us-west-2 ami-00f97030 aki-c2e26ff2 paravirtual
-raring server release 20121017 ebs amd64 test ami-40f97070 aki-98e26fa8 paravirtual
-raring server release 20121017 ebs i386 test ami-5ef9706e aki-c2e26ff2 paravirtual
-raring server release 20121017 instance-store amd64 test ami-50f97060 aki-98e26fa8 paravirtual
-raring server release 20121017 instance-store i386 test ami-00f97030 aki-c2e26ff2 paravirtual
=== modified file 'environs/interface.go'
--- environs/interface.go 2013-02-27 13:28:43 +0000
+++ environs/interface.go 2013-04-08 08:09:22 +0000
@@ -3,9 +3,11 @@
import (
"errors"
"io"
+ "launchpad.net/juju-core/constraints"
"launchpad.net/juju-core/environs/config"
"launchpad.net/juju-core/state"
"launchpad.net/juju-core/state/api"
+ "launchpad.net/juju-core/state/api/params"
)
// A EnvironProvider represents a computing and storage provider.
@@ -62,16 +64,16 @@
// OpenPorts opens the given ports on the instance, which
// should have been started with the given machine id.
- OpenPorts(machineId string, ports []state.Port) error
+ OpenPorts(machineId string, ports []params.Port) error
// ClosePorts closes the given ports on the instance, which
// should have been started with the given machine id.
- ClosePorts(machineId string, ports []state.Port) error
+ ClosePorts(machineId string, ports []params.Port) error
// Ports returns the set of ports open on the instance, which
// should have been started with the given machine id.
// The ports are returned as sorted by state.SortPorts.
- Ports(machineId string) ([]state.Port, error)
+ Ports(machineId string) ([]params.Port, error)
}
var ErrNoInstances = errors.New("no instances found")
@@ -136,19 +138,20 @@
Name() string
// Bootstrap initializes the state for the environment, possibly
- // starting one or more instances. If uploadTools is true, the
- // current version of the juju tools will be uploaded and used
- // on the environment's instances. If the configuration's
+ // starting one or more instances. If the configuration's
// AdminSecret is non-empty, the adminstrator password on the
// newly bootstrapped state will be set to a hash of it (see
// trivial.PasswordHash), When first connecting to the
// environment via the juju package, the password hash will be
// automatically replaced by the real password.
//
+ // The supplied constraints are used to choose the initial instance
+ // specification, and will be stored in the new environment's state.
+ //
// The stateServerCertand stateServerKey parameters hold
// both the certificate and the respective private key to be
// used by the initial state server, in PEM format.
- Bootstrap(uploadTools bool, stateServerCert, stateServerKey []byte) error
+ Bootstrap(cons constraints.Value, stateServerCert, stateServerKey []byte) error
// StateInfo returns information on the state initialized
// by Bootstrap.
@@ -170,8 +173,7 @@
// on the new machine are given by tools - if nil,
// the Environ will find a set of tools compatible with the
// current version.
- // TODO add arguments to specify type of new machine.
- StartInstance(machineId string, info *state.Info, apiInfo *api.Info, tools *state.Tools) (Instance, error)
+ StartInstance(machineId string, series string, cons constraints.Value, info *state.Info, apiInfo *api.Info) (Instance, error)
// StopInstances shuts down the given instances.
StopInstances([]Instance) error
@@ -211,17 +213,17 @@
// OpenPorts opens the given ports for the whole environment.
// Must only be used if the environment was setup with the
// FwGlobal firewall mode.
- OpenPorts(ports []state.Port) error
+ OpenPorts(ports []params.Port) error
// ClosePorts closes the given ports for the whole environment.
// Must only be used if the environment was setup with the
// FwGlobal firewall mode.
- ClosePorts(ports []state.Port) error
+ ClosePorts(ports []params.Port) error
// Ports returns the ports opened for the whole environment.
// Must only be used if the environment was setup with the
// FwGlobal firewall mode.
- Ports() ([]state.Port, error)
+ Ports() ([]params.Port, error)
// Provider returns the EnvironProvider that created this Environ.
Provider() EnvironProvider
=== modified file 'environs/jujutest/jujutest_test.go'
--- environs/jujutest/jujutest_test.go 2011-12-08 16:33:00 +0000
+++ environs/jujutest/jujutest_test.go 2013-04-08 08:09:22 +0000
@@ -1,8 +1,10 @@
package jujutest
-import "testing"
+import (
+ . "launchpad.net/gocheck"
+ "testing"
+)
-// A dummy test so that gotest succeeds when running
-// in this directory.
-func TestNothing(t *testing.T) {
+func Test(t *testing.T) {
+ TestingT(t)
}
=== modified file 'environs/jujutest/livetests.go'
--- environs/jujutest/livetests.go 2013-02-18 16:33:11 +0000
+++ environs/jujutest/livetests.go 2013-04-08 08:09:22 +0000
@@ -6,11 +6,13 @@
"io"
. "launchpad.net/gocheck"
"launchpad.net/juju-core/charm"
+ "launchpad.net/juju-core/constraints"
"launchpad.net/juju-core/environs"
"launchpad.net/juju-core/environs/config"
"launchpad.net/juju-core/juju"
"launchpad.net/juju-core/juju/testing"
"launchpad.net/juju-core/state"
+ "launchpad.net/juju-core/state/api/params"
coretesting "launchpad.net/juju-core/testing"
"launchpad.net/juju-core/trivial"
"launchpad.net/juju-core/version"
@@ -23,8 +25,8 @@
type LiveTests struct {
coretesting.LoggingSuite
- // Config holds the configuration attributes for opening an environment.
- Config map[string]interface{}
+ // TestConfig contains the configuration attributes for opening an environment.
+ TestConfig TestConfig
// Env holds the currently opened environment.
Env environs.Environ
@@ -46,8 +48,8 @@
func (t *LiveTests) SetUpSuite(c *C) {
t.LoggingSuite.SetUpSuite(c)
- e, err := environs.NewFromAttrs(t.Config)
- c.Assert(err, IsNil, Commentf("opening environ %#v", t.Config))
+ e, err := environs.NewFromAttrs(t.TestConfig.Config)
+ c.Assert(err, IsNil, Commentf("opening environ %#v", t.TestConfig.Config))
c.Assert(e, NotNil)
t.Env = e
c.Logf("environment configuration: %#v", publicAttrs(e))
@@ -79,15 +81,18 @@
if t.bootstrapped {
return
}
- err := environs.Bootstrap(t.Env, true, panicWrite)
+ // 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 := environs.PutTools(t.Env.Storage(), nil)
+ c.Assert(err, IsNil)
+ }
+ err := environs.Bootstrap(t.Env, cons)
c.Assert(err, IsNil)
t.bootstrapped = true
}
-func panicWrite(name string, cert, key []byte) error {
- panic("writeCertAndKey called unexpectedly")
-}
-
func (t *LiveTests) Destroy(c *C) {
err := t.Env.Destroy(nil)
c.Assert(err, IsNil)
@@ -97,8 +102,7 @@
// TestStartStop is similar to Tests.TestStartStop except
// that it does not assume a pristine environment.
func (t *LiveTests) TestStartStop(c *C) {
- inst, err := t.Env.StartInstance("0", testing.InvalidStateInfo("0"), testing.InvalidAPIInfo("0"), nil)
- c.Assert(err, IsNil)
+ inst := testing.StartInstance(c, t.Env, "0")
c.Assert(inst, NotNil)
id0 := inst.Id()
@@ -149,16 +153,14 @@
}
func (t *LiveTests) TestPorts(c *C) {
- inst1, err := t.Env.StartInstance("1", testing.InvalidStateInfo("1"), testing.InvalidAPIInfo("1"), nil)
- c.Assert(err, IsNil)
+ inst1 := testing.StartInstance(c, t.Env, "1")
c.Assert(inst1, NotNil)
defer t.Env.StopInstances([]environs.Instance{inst1})
ports, err := inst1.Ports("1")
c.Assert(err, IsNil)
c.Assert(ports, HasLen, 0)
- inst2, err := t.Env.StartInstance("2", testing.InvalidStateInfo("2"), testing.InvalidAPIInfo("2"), nil)
- c.Assert(err, IsNil)
+ inst2 := testing.StartInstance(c, t.Env, "2")
c.Assert(inst2, NotNil)
ports, err = inst2.Ports("2")
c.Assert(err, IsNil)
@@ -166,70 +168,70 @@
defer t.Env.StopInstances([]environs.Instance{inst2})
// Open some ports and check they're there.
- err = inst1.OpenPorts("1", []state.Port{{"udp", 67}, {"tcp", 45}})
+ err = inst1.OpenPorts("1", []params.Port{{"udp", 67}, {"tcp", 45}})
c.Assert(err, IsNil)
ports, err = inst1.Ports("1")
c.Assert(err, IsNil)
- c.Assert(ports, DeepEquals, []state.Port{{"tcp", 45}, {"udp", 67}})
+ c.Assert(ports, DeepEquals, []params.Port{{"tcp", 45}, {"udp", 67}})
ports, err = inst2.Ports("2")
c.Assert(err, IsNil)
c.Assert(ports, HasLen, 0)
- err = inst2.OpenPorts("2", []state.Port{{"tcp", 89}, {"tcp", 45}})
+ err = inst2.OpenPorts("2", []params.Port{{"tcp", 89}, {"tcp", 45}})
c.Assert(err, IsNil)
// Check there's no crosstalk to another machine
ports, err = inst2.Ports("2")
c.Assert(err, IsNil)
- c.Assert(ports, DeepEquals, []state.Port{{"tcp", 45}, {"tcp", 89}})
+ c.Assert(ports, DeepEquals, []params.Port{{"tcp", 45}, {"tcp", 89}})
ports, err = inst1.Ports("1")
c.Assert(err, IsNil)
- c.Assert(ports, DeepEquals, []state.Port{{"tcp", 45}, {"udp", 67}})
+ c.Assert(ports, DeepEquals, []params.Port{{"tcp", 45}, {"udp", 67}})
// Check that opening the same port again is ok.
oldPorts, err := inst2.Ports("2")
c.Assert(err, IsNil)
- err = inst2.OpenPorts("2", []state.Port{{"tcp", 45}})
+ err = inst2.OpenPorts("2", []params.Port{{"tcp", 45}})
c.Assert(err, IsNil)
ports, err = inst2.Ports("2")
c.Assert(err, IsNil)
c.Assert(ports, DeepEquals, oldPorts)
// Check that opening the same port again and another port is ok.
- err = inst2.OpenPorts("2", []state.Port{{"tcp", 45}, {"tcp", 99}})
+ err = inst2.OpenPorts("2", []params.Port{{"tcp", 45}, {"tcp", 99}})
c.Assert(err, IsNil)
ports, err = inst2.Ports("2")
c.Assert(err, IsNil)
- c.Assert(ports, DeepEquals, []state.Port{{"tcp", 45}, {"tcp", 89}, {"tcp", 99}})
+ c.Assert(ports, DeepEquals, []params.Port{{"tcp", 45}, {"tcp", 89}, {"tcp", 99}})
- err = inst2.ClosePorts("2", []state.Port{{"tcp", 45}, {"tcp", 99}})
+ err = inst2.ClosePorts("2", []params.Port{{"tcp", 45}, {"tcp", 99}})
c.Assert(err, IsNil)
// Check that we can close ports and that there's no crosstalk.
ports, err = inst2.Ports("2")
c.Assert(err, IsNil)
- c.Assert(ports, DeepEquals, []state.Port{{"tcp", 89}})
+ c.Assert(ports, DeepEquals, []params.Port{{"tcp", 89}})
ports, err = inst1.Ports("1")
c.Assert(err, IsNil)
- c.Assert(ports, DeepEquals, []state.Port{{"tcp", 45}, {"udp", 67}})
+ c.Assert(ports, DeepEquals, []params.Port{{"tcp", 45}, {"udp", 67}})
// Check that we can close multiple ports.
- err = inst1.ClosePorts("1", []state.Port{{"tcp", 45}, {"udp", 67}})
+ err = inst1.ClosePorts("1", []params.Port{{"tcp", 45}, {"udp", 67}})
c.Assert(err, IsNil)
ports, err = inst1.Ports("1")
c.Assert(ports, HasLen, 0)
// Check that we can close ports that aren't there.
- err = inst2.ClosePorts("2", []state.Port{{"tcp", 111}, {"udp", 222}})
+ err = inst2.ClosePorts("2", []params.Port{{"tcp", 111}, {"udp", 222}})
c.Assert(err, IsNil)
ports, err = inst2.Ports("2")
- c.Assert(ports, DeepEquals, []state.Port{{"tcp", 89}})
+ c.Assert(ports, DeepEquals, []params.Port{{"tcp", 89}})
// Check errors when acting on environment.
- err = t.Env.OpenPorts([]state.Port{{"tcp", 80}})
+ err = t.Env.OpenPorts([]params.Port{{"tcp", 80}})
c.Assert(err, ErrorMatches, `invalid firewall mode for opening ports on environment: "instance"`)
- err = t.Env.ClosePorts([]state.Port{{"tcp", 80}})
+ err = t.Env.ClosePorts([]params.Port{{"tcp", 80}})
c.Assert(err, ErrorMatches, `invalid firewall mode for closing ports on environment: "instance"`)
_, err = t.Env.Ports()
@@ -252,48 +254,46 @@
c.Assert(err, IsNil)
// Create instances and check open ports on both instances.
- inst1, err := t.Env.StartInstance("1", testing.InvalidStateInfo("1"), testing.InvalidAPIInfo("1"), nil)
- c.Assert(err, IsNil)
+ inst1 := testing.StartInstance(c, t.Env, "1")
defer t.Env.StopInstances([]environs.Instance{inst1})
ports, err := t.Env.Ports()
c.Assert(err, IsNil)
c.Assert(ports, HasLen, 0)
- inst2, err := t.Env.StartInstance("2", testing.InvalidStateInfo("2"), testing.InvalidAPIInfo("2"), nil)
- c.Assert(err, IsNil)
+ inst2 := testing.StartInstance(c, t.Env, "2")
ports, err = t.Env.Ports()
c.Assert(err, IsNil)
c.Assert(ports, HasLen, 0)
defer t.Env.StopInstances([]environs.Instance{inst2})
- err = t.Env.OpenPorts([]state.Port{{"udp", 67}, {"tcp", 45}, {"tcp", 89}, {"tcp", 99}})
+ err = t.Env.OpenPorts([]params.Port{{"udp", 67}, {"tcp", 45}, {"tcp", 89}, {"tcp", 99}})
c.Assert(err, IsNil)
ports, err = t.Env.Ports()
c.Assert(err, IsNil)
- c.Assert(ports, DeepEquals, []state.Port{{"tcp", 45}, {"tcp", 89}, {"tcp", 99}, {"udp", 67}})
+ c.Assert(ports, DeepEquals, []params.Port{{"tcp", 45}, {"tcp", 89}, {"tcp", 99}, {"udp", 67}})
// Check closing some ports.
- err = t.Env.ClosePorts([]state.Port{{"tcp", 99}, {"udp", 67}})
+ err = t.Env.ClosePorts([]params.Port{{"tcp", 99}, {"udp", 67}})
c.Assert(err, IsNil)
ports, err = t.Env.Ports()
c.Assert(err, IsNil)
- c.Assert(ports, DeepEquals, []state.Port{{"tcp", 45}, {"tcp", 89}})
+ c.Assert(ports, DeepEquals, []params.Port{{"tcp", 45}, {"tcp", 89}})
// Check that we can close ports that aren't there.
- err = t.Env.ClosePorts([]state.Port{{"tcp", 111}, {"udp", 222}})
+ err = t.Env.ClosePorts([]params.Port{{"tcp", 111}, {"udp", 222}})
c.Assert(err, IsNil)
ports, err = t.Env.Ports()
c.Assert(err, IsNil)
- c.Assert(ports, DeepEquals, []state.Port{{"tcp", 45}, {"tcp", 89}})
+ c.Assert(ports, DeepEquals, []params.Port{{"tcp", 45}, {"tcp", 89}})
// Check errors when acting on instances.
- err = inst1.OpenPorts("1", []state.Port{{"tcp", 80}})
+ err = inst1.OpenPorts("1", []params.Port{{"tcp", 80}})
c.Assert(err, ErrorMatches, `invalid firewall mode for opening ports on instance: "global"`)
- err = inst1.ClosePorts("1", []state.Port{{"tcp", 80}})
+ err = inst1.ClosePorts("1", []params.Port{{"tcp", 80}})
c.Assert(err, ErrorMatches, `invalid firewall mode for closing ports on instance: "global"`)
_, err = inst1.Ports("1")
@@ -303,7 +303,7 @@
func (t *LiveTests) TestBootstrapMultiple(c *C) {
t.BootstrapOnce(c)
- err := environs.Bootstrap(t.Env, false, panicWrite)
+ err := environs.Bootstrap(t.Env, constraints.Value{})
c.Assert(err, ErrorMatches, "environment is already bootstrapped")
c.Logf("destroy env")
@@ -336,7 +336,12 @@
// bootstrap process (it's optional in the config.Config)
cfg, err := conn.State.EnvironConfig()
c.Assert(err, IsNil)
- c.Check(cfg.AgentVersion(), Equals, version.Current.Number)
+ c.Check(cfg.AgentVersion(), Equals, version.CurrentNumber())
+
+ // Check that the constraints have been set in the environment.
+ cons, err := conn.State.EnvironConstraints()
+ c.Assert(err, IsNil)
+ c.Assert(cons.String(), Equals, "mem=2048M")
// Wait for machine agent to come up on the bootstrap
// machine and find the deployed series from that.
@@ -404,15 +409,37 @@
// Now remove the unit and its assigned machine and
// check that the PA removes it.
c.Logf("removing unit")
- err = unit.EnsureDead()
- c.Assert(err, IsNil)
- err = unit.Remove()
- c.Assert(err, IsNil)
- err = m1.EnsureDead()
- c.Assert(err, IsNil)
- err = m1.Remove()
+ err = unit.Destroy()
c.Assert(err, IsNil)
+ // Wait until unit is dead
+ uwatch := unit.Watch()
+ defer uwatch.Stop()
+ for unit.Life() != state.Dead {
+ c.Logf("waiting for unit change")
+ <-uwatch.Changes()
+ err := unit.Refresh()
+ c.Logf("refreshed; err %v", err)
+ if state.IsNotFound(err) {
+ c.Logf("unit has been removed")
+ break
+ }
+ c.Assert(err, IsNil)
+ }
+ for {
+ c.Logf("destroying machine")
+ err := m1.Destroy()
+ if err == nil {
+ break
+ }
+ c.Assert(err, FitsTypeOf, &state.HasAssignedUnitsError{})
+ time.Sleep(5 * time.Second)
+ err = m1.Refresh()
+ if state.IsNotFound(err) {
+ break
+ }
+ c.Assert(err, IsNil)
+ }
c.Logf("waiting for instance to be removed")
t.assertStopInstance(c, conn.Environ, instId1)
}
@@ -652,35 +679,19 @@
c.Check(err, IsNil)
}
-// Check that we can't start an instance running tools
-// that correspond with no available platform.
+// Check that we can't start an instance running tools that correspond with no
+// available platform. The first thing start instance should do is find
+// appropriate tools.
func (t *LiveTests) TestStartInstanceOnUnknownPlatform(c *C) {
- vers := version.Current
- // Note that we want this test to function correctly in the
- // dummy environment, so to avoid enumerating all possible
- // platforms in the dummy provider, it treats only series and/or
- // architectures with the "unknown" prefix as invalid.
- vers.Series = "unknownseries"
- vers.Arch = "unknownarch"
- name := environs.ToolsStoragePath(vers)
- storage := t.Env.Storage()
- checkPutFile(c, storage, name, []byte("fake tools on invalid series"))
- defer storage.Remove(name)
-
- url, err := storage.URL(name)
- c.Assert(err, IsNil)
- tools := &state.Tools{
- Binary: vers,
- URL: url,
- }
-
- inst, err := t.Env.StartInstance("4", testing.InvalidStateInfo("4"), testing.InvalidAPIInfo("4"), tools)
+ inst, err := t.Env.StartInstance("4", "unknownseries", constraints.Value{}, testing.InvalidStateInfo("4"), testing.InvalidAPIInfo("4"))
if inst != nil {
err := t.Env.StopInstances([]environs.Instance{inst})
c.Check(err, IsNil)
}
c.Assert(inst, IsNil)
- c.Assert(err, ErrorMatches, "cannot find image.*")
+ var notFoundError *environs.NotFoundError
+ c.Assert(err, FitsTypeOf, notFoundError)
+ c.Assert(err, ErrorMatches, "no compatible tools found")
}
func (t *LiveTests) TestBootstrapWithDefaultSeries(c *C) {
@@ -702,10 +713,12 @@
c.Assert(err, IsNil)
dummyenv, err := environs.NewFromAttrs(map[string]interface{}{
- "type": "dummy",
- "name": "dummy storage",
- "secret": "pizza",
- "state-server": false,
+ "type": "dummy",
+ "name": "dummy storage",
+ "secret": "pizza",
+ "state-server": false,
+ "ca-cert": coretesting.CACert,
+ "ca-private-key": coretesting.CAKey,
})
c.Assert(err, IsNil)
defer dummyenv.Destroy(nil)
@@ -733,7 +746,7 @@
err = storageCopy(dummyStorage, currentPath, envStorage, otherPath)
c.Assert(err, IsNil)
- err = environs.Bootstrap(env, false, panicWrite)
+ err = environs.Bootstrap(env, constraints.Value{})
c.Assert(err, IsNil)
defer env.Destroy(nil)
=== added file 'environs/jujutest/metadata.go'
--- environs/jujutest/metadata.go 1970-01-01 00:00:00 +0000
+++ environs/jujutest/metadata.go 2013-04-08 08:09:22 +0000
@@ -0,0 +1,70 @@
+package jujutest
+
+import (
+ "io/ioutil"
+ "net/http"
+ "strings"
+)
+
+// VirtualRoundTripper can be used to provide "http" responses without actually
+// starting an HTTP server. It is used by calling:
+// vfs := NewVirtualRoundTripper([]FileContent{<file contents>})
+// http.DefaultTransport.(*http.Transport).RegisterProtocol("test", vfs)
+// At which point requests to test:///foo will pull out the virtual content of
+// the file named 'foo' passed into the RoundTripper constructor.
+type VirtualRoundTripper struct {
+ contents []FileContent
+}
+
+var _ http.RoundTripper = (*VirtualRoundTripper)(nil)
+
+// When using RegisterProtocol on http.Transport, you can't actually change the
+// registration. So we provide a RoundTripper that simply proxies to whatever
+// we want as the current content.
+type ProxyRoundTripper struct {
+ Sub http.RoundTripper
+}
+
+var _ http.RoundTripper = (*ProxyRoundTripper)(nil)
+
+func (prt *ProxyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+ if prt.Sub == nil {
+ panic("An attempt was made to request file content without having" +
+ " the virtual filesystem initialized.")
+ }
+ return prt.Sub.RoundTrip(req)
+}
+
+// A simple content structure to pass data into VirtualRoundTripper. When using
+// VRT, requests that match 'Name' will be served the value in 'Content'
+type FileContent struct {
+ Name string
+ Content string
+}
+
+// Map the Path into Content based on FileContent.Name
+func (v *VirtualRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+ res := &http.Response{Proto: "HTTP/1.0",
+ ProtoMajor: 1,
+ Header: make(http.Header),
+ Close: true,
+ }
+ for _, fc := range v.contents {
+ if fc.Name == req.URL.Path {
+ res.Status = "200 OK"
+ res.StatusCode = http.StatusOK
+ res.ContentLength = int64(len(fc.Content))
+ res.Body = ioutil.NopCloser(strings.NewReader(fc.Content))
+ return res, nil
+ }
+ }
+ res.Status = "404 Not Found"
+ res.StatusCode = http.StatusNotFound
+ res.ContentLength = 0
+ res.Body = ioutil.NopCloser(strings.NewReader(""))
+ return res, nil
+}
+
+func NewVirtualRoundTripper(contents []FileContent) *VirtualRoundTripper {
+ return &VirtualRoundTripper{contents}
+}
=== added file 'environs/jujutest/metadata_test.go'
--- environs/jujutest/metadata_test.go 1970-01-01 00:00:00 +0000
+++ environs/jujutest/metadata_test.go 2013-04-08 08:09:22 +0000
@@ -0,0 +1,46 @@
+package jujutest
+
+import (
+ "io/ioutil"
+ . "launchpad.net/gocheck"
+ "net/http"
+ "net/url"
+)
+
+type metadataSuite struct{}
+
+var _ = Suite(&metadataSuite{})
+
+func (s *metadataSuite) TestVirtualRoundTripper(c *C) {
+ aContent := "a-content"
+ vrt := NewVirtualRoundTripper([]FileContent{
+ {"a", aContent},
+ {"b", "b-content"},
+ })
+ c.Assert(vrt, NotNil)
+ req := &http.Request{URL: &url.URL{Path: "a"}}
+ resp, err := vrt.RoundTrip(req)
+ c.Assert(err, IsNil)
+ c.Assert(resp, NotNil)
+ content, err := ioutil.ReadAll(resp.Body)
+ c.Assert(string(content), Equals, aContent)
+ c.Assert(resp.ContentLength, Equals, int64(len(aContent)))
+ c.Assert(resp.StatusCode, Equals, http.StatusOK)
+ c.Assert(resp.Status, Equals, "200 OK")
+}
+
+func (s *metadataSuite) TestVirtualRoundTripperMissing(c *C) {
+ vrt := NewVirtualRoundTripper([]FileContent{
+ {"a", "a-content"},
+ })
+ c.Assert(vrt, NotNil)
+ req := &http.Request{URL: &url.URL{Path: "no-such-file"}}
+ resp, err := vrt.RoundTrip(req)
+ c.Assert(err, IsNil)
+ c.Assert(resp, NotNil)
+ content, err := ioutil.ReadAll(resp.Body)
+ c.Assert(string(content), Equals, "")
+ c.Assert(resp.ContentLength, Equals, int64(0))
+ c.Assert(resp.StatusCode, Equals, http.StatusNotFound)
+ c.Assert(resp.Status, Equals, "404 Not Found")
+}
=== modified file 'environs/jujutest/tests.go'
--- environs/jujutest/tests.go 2013-03-04 22:25:23 +0000
+++ environs/jujutest/tests.go 2013-04-08 08:09:22 +0000
@@ -5,14 +5,35 @@
"io"
"io/ioutil"
. "launchpad.net/gocheck"
+ "launchpad.net/juju-core/constraints"
"launchpad.net/juju-core/environs"
"launchpad.net/juju-core/juju/testing"
"launchpad.net/juju-core/state"
coretesting "launchpad.net/juju-core/testing"
"launchpad.net/juju-core/trivial"
"net/http"
+ "sort"
)
+// TestConfig contains the configuration for the environment
+// This is a is an indirection to make it harder for tests to accidentally
+// share the underlying map.
+type TestConfig struct {
+ Config map[string]interface{}
+}
+
+// UpdateConfig modifies the configuration safely by creating a new map
+func (testConfig *TestConfig) UpdateConfig(update map[string]interface{}) {
+ newConfig := map[string]interface{}{}
+ for key, val := range testConfig.Config {
+ newConfig[key] = val
+ }
+ for key, val := range update {
+ newConfig[key] = val
+ }
+ testConfig.Config = newConfig
+}
+
// Tests is a gocheck suite containing tests verifying juju functionality
// against the environment with the given configuration. The
// tests are not designed to be run against a live server - the Environ
@@ -20,14 +41,14 @@
// may be executed.
type Tests struct {
coretesting.LoggingSuite
- Config map[string]interface{}
- Env environs.Environ
+ TestConfig TestConfig
+ Env environs.Environ
}
// Open opens an instance of the testing environment.
func (t *Tests) Open(c *C) environs.Environ {
- e, err := environs.NewFromAttrs(t.Config)
- c.Assert(err, IsNil, Commentf("opening environ %#v", t.Config))
+ e, err := environs.NewFromAttrs(t.TestConfig.Config)
+ c.Assert(err, IsNil, Commentf("opening environ %#v", t.TestConfig.Config))
c.Assert(e, NotNil)
return e
}
@@ -51,16 +72,10 @@
delete(m, "admin-secret")
env, err := environs.NewFromAttrs(m)
c.Assert(err, IsNil)
- err = environs.Bootstrap(env, false, panicWrite)
+ err = environs.Bootstrap(env, constraints.Value{})
c.Assert(err, ErrorMatches, ".*admin-secret is required for bootstrap")
}
-func (t *Tests) TestProviderAssignmentPolicy(c *C) {
- e := t.Open(c)
- policy := e.AssignmentPolicy()
- c.Assert(policy, FitsTypeOf, state.AssignUnused)
-}
-
func (t *Tests) TestStartStop(c *C) {
e := t.Open(c)
@@ -68,13 +83,11 @@
c.Assert(err, IsNil)
c.Assert(insts, HasLen, 0)
- inst0, err := e.StartInstance("0", testing.InvalidStateInfo("0"), testing.InvalidAPIInfo("0"), nil)
- c.Assert(err, IsNil)
+ inst0 := testing.StartInstance(c, e, "0")
c.Assert(inst0, NotNil)
id0 := inst0.Id()
- inst1, err := e.StartInstance("1", testing.InvalidStateInfo("1"), testing.InvalidAPIInfo("1"), nil)
- c.Assert(err, IsNil)
+ inst1 := testing.StartInstance(c, e, "1")
c.Assert(inst1, NotNil)
id1 := inst1.Id()
@@ -106,18 +119,18 @@
func (t *Tests) TestBootstrap(c *C) {
// TODO tests for Bootstrap(true)
e := t.Open(c)
- err := environs.Bootstrap(e, false, panicWrite)
+ err := environs.Bootstrap(e, constraints.Value{})
c.Assert(err, IsNil)
info, apiInfo, err := e.StateInfo()
c.Check(info.Addrs, Not(HasLen), 0)
c.Check(apiInfo.Addrs, Not(HasLen), 0)
- err = environs.Bootstrap(e, false, panicWrite)
+ err = environs.Bootstrap(e, constraints.Value{})
c.Assert(err, ErrorMatches, "environment is already bootstrapped")
e2 := t.Open(c)
- err = environs.Bootstrap(e2, false, panicWrite)
+ err = environs.Bootstrap(e2, constraints.Value{})
c.Assert(err, ErrorMatches, "environment is already bootstrapped")
info2, apiInfo2, err := e2.StateInfo()
@@ -130,10 +143,10 @@
// Open again because Destroy invalidates old environments.
e3 := t.Open(c)
- err = environs.Bootstrap(e3, false, panicWrite)
+ err = environs.Bootstrap(e3, constraints.Value{})
c.Assert(err, IsNil)
- err = environs.Bootstrap(e3, false, panicWrite)
+ err = environs.Bootstrap(e3, constraints.Value{})
c.Assert(err, NotNil)
}
@@ -186,7 +199,19 @@
func checkList(c *C, storage environs.StorageReader, prefix string, names []string) {
lnames, err := storage.List(prefix)
c.Assert(err, IsNil)
- c.Assert(lnames, DeepEquals, names)
+ // TODO(dfc) gocheck should grow an SliceEquals checker.
+ expected := copyslice(lnames)
+ sort.Strings(expected)
+ actual := copyslice(names)
+ sort.Strings(actual)
+ c.Assert(expected, DeepEquals, actual)
+}
+
+// copyslice returns a copy of the slice
+func copyslice(s []string) []string {
+ r := make([]string, len(s))
+ copy(r, s)
+ return r
}
func checkPutFile(c *C, storage environs.StorageWriter, name string, contents []byte) {
=== modified file 'environs/local/backend_test.go'
--- environs/local/backend_test.go 2013-02-22 13:59:18 +0000
+++ environs/local/backend_test.go 2013-04-08 08:09:22 +0000
@@ -10,6 +10,7 @@
"os"
"path/filepath"
"strings"
+ "sync"
"testing"
"launchpad.net/juju-core/environs/local"
@@ -19,29 +20,25 @@
TestingT(t)
}
-type backendSuite struct {
- listener net.Listener
- dataDir string
-}
+type backendSuite struct{}
var _ = Suite(&backendSuite{})
-const (
- environName = "test-environ"
- portNo = 60006
-)
-
-func (s *backendSuite) SetUpSuite(c *C) {
- var err error
- s.dataDir = c.MkDir()
- s.listener, err = local.Listen(s.dataDir, environName, "127.0.0.1", portNo)
+const environName = "test-environ"
+
+var testSetMu sync.Mutex
+
+// nextTestSet returns a new port number, listener and data directory.
+func nextTestSet(c *C) (int, net.Listener, string) {
+ testSetMu.Lock()
+ defer testSetMu.Unlock()
+
+ dataDir := c.MkDir()
+ listener, err := local.Listen(dataDir, environName, "127.0.0.1", 0)
c.Assert(err, IsNil)
-
- createTestData(c, s.dataDir)
-}
-
-func (s *backendSuite) TearDownSuite(c *C) {
- s.listener.Close()
+ port := listener.Addr().(*net.TCPAddr).Port
+
+ return port, listener, dataDir
}
type testCase struct {
@@ -116,6 +113,11 @@
func (s *backendSuite) TestGet(c *C) {
// Test retrieving a file from a storage.
+ portNo, listener, dataDir := nextTestSet(c)
+ defer listener.Close()
+
+ createTestData(c, dataDir)
+
check := func(tc testCase) {
url := fmt.Sprintf("http://localhost:%d/%s", portNo, tc.name)
resp, err := http.Get(url)
@@ -182,6 +184,11 @@
func (s *backendSuite) TestList(c *C) {
// Test listing file of a storage.
+ portNo, listener, dataDir := nextTestSet(c)
+ defer listener.Close()
+
+ createTestData(c, dataDir)
+
check := func(tc testCase) {
url := fmt.Sprintf("http://localhost:%d/%s*", portNo, tc.name)
resp, err := http.Get(url)
@@ -224,6 +231,11 @@
func (s *backendSuite) TestPut(c *C) {
// Test sending a file to the storage.
+ portNo, listener, dataDir := nextTestSet(c)
+ defer listener.Close()
+
+ createTestData(c, dataDir)
+
check := func(tc testCase) {
url := fmt.Sprintf("http://localhost:%d/%s", portNo, tc.name)
req, err := http.NewRequest("PUT", url, bytes.NewBufferString(tc.content))
@@ -237,7 +249,7 @@
}
c.Assert(resp.StatusCode, Equals, 201)
- fp := filepath.Join(s.dataDir, environName, tc.name)
+ fp := filepath.Join(dataDir, environName, tc.name)
b, err := ioutil.ReadFile(fp)
c.Assert(err, IsNil)
c.Assert(string(b), Equals, tc.content)
@@ -273,8 +285,13 @@
func (s *backendSuite) TestRemove(c *C) {
// Test removing a file in the storage.
+ portNo, listener, dataDir := nextTestSet(c)
+ defer listener.Close()
+
+ createTestData(c, dataDir)
+
check := func(tc testCase) {
- fp := filepath.Join(s.dataDir, environName, tc.name)
+ fp := filepath.Join(dataDir, environName, tc.name)
dir, _ := filepath.Split(fp)
err := os.MkdirAll(dir, 0777)
c.Assert(err, IsNil)
=== modified file 'environs/local/storage_test.go'
--- environs/local/storage_test.go 2013-02-24 04:37:34 +0000
+++ environs/local/storage_test.go 2013-04-08 08:09:22 +0000
@@ -16,14 +16,10 @@
// TestPersistence tests the adding, reading, listing and removing
// of files from the local storage.
func (s *storageSuite) TestPersistence(c *C) {
- // Non-standard port to avoid conflict with not-yet full
- // closed listener in backend test.
- portNo := 60007
- listener, err := local.Listen(c.MkDir(), environName, "127.0.0.1", portNo)
- c.Assert(err, IsNil)
+ portNo, listener, _ := nextTestSet(c)
defer listener.Close()
+
storage := local.NewStorage("127.0.0.1", portNo)
-
names := []string{
"aa",
"zzz/aa",
@@ -43,7 +39,7 @@
}
// remove the first file and check that the others remain.
- err = storage2.Remove(names[0])
+ err := storage2.Remove(names[0])
c.Check(err, IsNil)
// check that it's ok to remove a file twice.
=== modified file 'environs/maas/config_test.go'
--- environs/maas/config_test.go 2013-02-06 11:29:19 +0000
+++ environs/maas/config_test.go 2013-04-08 08:09:22 +0000
@@ -42,9 +42,10 @@
oauth := "consumer-key:resource-token:resource-secret"
secret := "ssssssht"
ecfg, err := newConfig(map[string]interface{}{
- "maas-server": server,
- "maas-oauth": oauth,
- "admin-secret": secret,
+ "maas-server": server,
+ "maas-oauth": oauth,
+ "admin-secret": secret,
+ "authorized-keys": "I-am-not-a-real-key",
})
c.Assert(err, IsNil)
c.Check(ecfg.MAASServer(), Equals, server)
@@ -54,9 +55,10 @@
func (ConfigSuite) TestChecksWellFormedMaasServer(c *C) {
_, err := newConfig(map[string]interface{}{
- "maas-server": "This should have been a URL.",
- "maas-oauth": "consumer-key:resource-token:resource-secret",
- "admin-secret": "secret",
+ "maas-server": "This should have been a URL.",
+ "maas-oauth": "consumer-key:resource-token:resource-secret",
+ "admin-secret": "secret",
+ "authorized-keys": "I-am-not-a-real-key",
})
c.Assert(err, NotNil)
c.Check(err, ErrorMatches, ".*malformed maas-server.*")
@@ -64,9 +66,10 @@
func (ConfigSuite) TestChecksWellFormedMaasOAuth(c *C) {
_, err := newConfig(map[string]interface{}{
- "maas-server": "http://maas.example.com/maas/",
- "maas-oauth": "This should have been a 3-part token.",
- "admin-secret": "secret",
+ "maas-server": "http://maas.example.com/maas/",
+ "maas-oauth": "This should have been a 3-part token.",
+ "admin-secret": "secret",
+ "authorized-keys": "I-am-not-a-real-key",
})
c.Assert(err, NotNil)
c.Check(err, ErrorMatches, ".*malformed maas-oauth.*")
=== modified file 'environs/maas/environ.go'
--- environs/maas/environ.go 2013-04-05 07:46:50 +0000
+++ environs/maas/environ.go 2013-04-08 08:09:22 +0000
@@ -5,12 +5,14 @@
"errors"
"fmt"
"launchpad.net/gomaasapi"
+ "launchpad.net/juju-core/constraints"
"launchpad.net/juju-core/environs"
"launchpad.net/juju-core/environs/cloudinit"
"launchpad.net/juju-core/environs/config"
"launchpad.net/juju-core/log"
"launchpad.net/juju-core/state"
"launchpad.net/juju-core/state/api"
+ "launchpad.net/juju-core/state/api/params"
"launchpad.net/juju-core/trivial"
"launchpad.net/juju-core/version"
"net/url"
@@ -118,10 +120,7 @@
// getMongoURL returns the URL to the appropriate MongoDB instance.
func (env *maasEnviron) getMongoURL(tools *state.Tools) string {
- v := version.Current
- v.Series = tools.Series
- v.Arch = tools.Arch
- return environs.MongoURL(env, v)
+ return environs.MongoURL(env, tools.Series, tools.Arch)
}
// makeMachineConfig sets up a basic machine configuration for use with
@@ -184,13 +183,16 @@
}
// Bootstrap is specified in the Environ interface.
-func (env *maasEnviron) Bootstrap(uploadTools bool, stateServerCert, stateServerKey []byte) error {
+func (env *maasEnviron) Bootstrap(cons constraints.Value, stateServerCert, stateServerKey []byte) error {
+ // TODO: Fix this quick hack. uploadTools is a now-obsolete parameter.
+ uploadTools := false
+
// This was all cargo-culted from the EC2 provider.
password := env.Config().AdminSecret()
if password == "" {
return fmt.Errorf("admin-secret is required for bootstrap")
}
- log.Printf("environs/maas: bootstrapping environment %q.", env.Name())
+ log.Debugf("environs/maas: bootstrapping environment %q.", env.Name())
err := env.quiesceStateFile()
if err != nil {
return err
@@ -239,7 +241,7 @@
var apiAddrs []string
// Wait for the DNS names of any of the instances
// to become available.
- log.Printf("environs/maas: waiting for DNS name(s) of state server instances %v", st.StateInstances)
+ log.Debugf("environs/maas: waiting for DNS name(s) of state server instances %v", st.StateInstances)
for a := longAttempt.Start(); len(stateAddrs) == 0 && a.Next(); {
insts, err := env.Instances(st.StateInstances)
if err != nil && err != environs.ErrPartialInstances {
@@ -359,7 +361,7 @@
// implementation of StartInstance, and to initialize the bootstrap node.
func (environ *maasEnviron) obtainNode(machineId string, stateInfo *state.Info, apiInfo *api.Info, tools *state.Tools, mcfg *cloudinit.MachineConfig) (*maasInstance, error) {
- log.Printf("environs/maas: starting machine %s in $q running tools version %q from %q", machineId, environ.name, tools.Binary, tools.URL)
+ log.Debugf("environs/maas: starting machine %s in $q running tools version %q from %q", machineId, environ.name, tools.Binary, tools.URL)
node, err := environ.acquireNode()
if err != nil {
@@ -378,19 +380,18 @@
environ.StopInstances([]environs.Instance{&instance})
return nil, fmt.Errorf("cannot start instance: %v", err)
}
- log.Printf("environs/maas: started instance %q", instance.Id())
+ log.Debugf("environs/maas: started instance %q", instance.Id())
return &instance, nil
}
// StartInstance is specified in the Environ interface.
-func (environ *maasEnviron) StartInstance(machineID string, stateInfo *state.Info, apiInfo *api.Info, tools *state.Tools) (environs.Instance, error) {
- if tools == nil {
- flags := environs.HighestVersion | environs.CompatVersion
- var err error
- tools, err = environs.FindTools(environ, version.Current, flags)
- if err != nil {
- return nil, err
- }
+func (environ *maasEnviron) StartInstance(machineID string, series string, cons constraints.Value, stateInfo *state.Info, apiInfo *api.Info) (environs.Instance, error) {
+ // TODO: Support series.
+ flags := environs.HighestVersion | environs.CompatVersion
+ var err error
+ tools, err := environs.FindTools(environ, version.Current, flags)
+ if err != nil {
+ return nil, err
}
mcfg := environ.makeMachineConfig(machineID, stateInfo, apiInfo, tools)
@@ -502,7 +503,7 @@
}
func (environ *maasEnviron) Destroy(ensureInsts []environs.Instance) error {
- log.Printf("environs/maas: destroying environment %q", environ.name)
+ log.Debugf("environs/maas: destroying environment %q", environ.name)
insts, err := environ.AllInstances()
if err != nil {
return fmt.Errorf("cannot get instances: %v", err)
@@ -537,15 +538,15 @@
return state.AssignUnused
}
-func (*maasEnviron) OpenPorts([]state.Port) error {
- panic("Not implemented.")
-}
-
-func (*maasEnviron) ClosePorts([]state.Port) error {
- panic("Not implemented.")
-}
-
-func (*maasEnviron) Ports() ([]state.Port, error) {
+func (*maasEnviron) OpenPorts([]params.Port) error {
+ panic("Not implemented.")
+}
+
+func (*maasEnviron) ClosePorts([]params.Port) error {
+ panic("Not implemented.")
+}
+
+func (*maasEnviron) Ports() ([]params.Port, error) {
panic("Not implemented.")
}
=== modified file 'environs/maas/environ_test.go'
--- environs/maas/environ_test.go 2013-04-03 15:04:01 +0000
+++ environs/maas/environ_test.go 2013-04-08 08:09:22 +0000
@@ -5,10 +5,13 @@
"fmt"
. "launchpad.net/gocheck"
"launchpad.net/gomaasapi"
+ "launchpad.net/juju-core/constraints"
"launchpad.net/juju-core/environs"
"launchpad.net/juju-core/environs/config"
+ envtesting "launchpad.net/juju-core/environs/testing"
"launchpad.net/juju-core/state"
"launchpad.net/juju-core/testing"
+ "launchpad.net/juju-core/version"
)
type EnvironSuite struct {
@@ -20,10 +23,11 @@
// getTestConfig creates a customized sample MAAS provider configuration.
func getTestConfig(name, server, oauth, secret string) *config.Config {
ecfg, err := newConfig(map[string]interface{}{
- "name": name,
- "maas-server": server,
- "maas-oauth": oauth,
- "admin-secret": secret,
+ "name": name,
+ "maas-server": server,
+ "maas-oauth": oauth,
+ "admin-secret": secret,
+ "authorized-keys": "I-am-not-a-real-key",
})
if err != nil {
panic(err)
@@ -54,6 +58,15 @@
return env
}
+func (suite *EnvironSuite) setupFakeProviderStateFile(c *C) {
+ suite.testMAASObject.TestServer.NewFile("provider-state", []byte("test file content"))
+}
+
+func (suite *EnvironSuite) setupFakeTools(c *C) {
+ storage := NewStorage(suite.environ)
+ envtesting.PutFakeTools(c, storage)
+}
+
func (EnvironSuite) TestSetConfigUpdatesConfig(c *C) {
cfg := getTestConfig("test env", "http://maas2.example.com", "a:b:c", "secret")
env, err := NewEnviron(cfg)
@@ -176,17 +189,34 @@
return fmt.Errorf("unexpected call to writeCertAndKey")
}
-func (suite *EnvironSuite) TestStartInstanceStartsInstance(c *C) {
+func (suite *EnvironSuite) SetUpBootstrapNode(c *C, hostname string, environ *maasEnviron) {
+ input := `{"system_id": "bootstrap-sys-id", "hostname": "` + hostname + `"}`
+ node := suite.testMAASObject.TestServer.NewNode(input)
+ instance := &maasInstance{&node, suite.environ}
+ err := environ.saveState(&bootstrapState{StateInstances: []state.InstanceId{instance.Id()}})
+ c.Assert(err, IsNil)
+}
+
+// TODO: this test fails from time to time: we need to investigate what's
+// going on (also see the additional remarks below).
+func (suite *EnvironSuite) DisableTestStartInstanceStartsInstance(c *C) {
+ suite.setupFakeTools(c)
+ env := suite.makeEnviron()
+ suite.setupFakeProviderStateFile(c)
suite.testMAASObject.TestServer.NewNode(`{"system_id": "node1", "hostname": "host1"}`)
- suite.testMAASObject.TestServer.NewNode(`{"system_id": "node2", "hostname": "host2"}`)
- env := suite.makeEnviron()
- err := environs.Bootstrap(env, true, fakeWriteCertAndKey)
+ suite.SetUpBootstrapNode(c, "test", env)
+ // TODO: This node, I suspect, was here to make sure that it was *not*
+ // started but the test did the opposite (and was passing!)!
+ // See below.
+ //suite.testMAASObject.TestServer.NewNode(`{"system_id": "node2", "hostname": "host2"}`)
+ err := environs.Bootstrap(env, constraints.Value{})
stateInfo, apiInfo, err := env.StateInfo()
c.Assert(err, IsNil)
- stateInfo.EntityName = "machine-1"
- apiInfo.EntityName = "machine-1"
+ stateInfo.Tag = "machine-1"
+ apiInfo.Tag = "machine-1"
- instance, err := env.StartInstance("1", stateInfo, apiInfo, nil)
+ series := version.Current.Series
+ instance, err := env.StartInstance("1", series, constraints.Value{}, stateInfo, apiInfo)
c.Assert(err, IsNil)
c.Check(instance, NotNil)
@@ -194,9 +224,11 @@
actions, found := operations["node1"]
c.Check(found, Equals, true)
c.Check(actions, DeepEquals, []string{"start"})
- actions, found = operations["node2"]
- c.Check(found, Equals, true)
- c.Check(actions, DeepEquals, []string{"start"})
+ // TODO: figure out how this was passing: we're only starting one
+ // instance so the only node1 should be started!!!
+ //actions, found = operations["node2"]
+ //c.Check(found, Equals, true)
+ //c.Check(actions, DeepEquals, []string{"start"})
}
func (suite *EnvironSuite) getInstance(systemId string) *maasInstance {
@@ -304,31 +336,34 @@
// at the time of writing that would require more support from gomaasapi's
// testing service than we have.
func (suite *EnvironSuite) TestBootstrapSucceeds(c *C) {
+ suite.setupFakeTools(c)
env := suite.makeEnviron()
suite.testMAASObject.TestServer.NewNode(`{"system_id": "thenode"}`)
cert := []byte{1, 2, 3}
key := []byte{4, 5, 6}
- err := env.Bootstrap(true, cert, key)
+ err := env.Bootstrap(constraints.Value{}, cert, key)
c.Assert(err, IsNil)
}
func (suite *EnvironSuite) TestBootstrapFailsIfNoNodes(c *C) {
+ suite.setupFakeTools(c)
env := suite.makeEnviron()
cert := []byte{1, 2, 3}
key := []byte{4, 5, 6}
- err := env.Bootstrap(true, cert, key)
+ err := env.Bootstrap(constraints.Value{}, cert, key)
// Since there are no nodes, the attempt to allocate one returns a
// 409: Conflict.
c.Check(err, ErrorMatches, ".*409.*")
}
func (suite *EnvironSuite) TestBootstrapIntegratesWithEnvirons(c *C) {
+ suite.setupFakeTools(c)
env := suite.makeEnviron()
suite.testMAASObject.TestServer.NewNode(`{"system_id": "bootstrapnode"}`)
// environs.Bootstrap calls Environ.Bootstrap. This works.
- err := environs.Bootstrap(env, true, fakeWriteCertAndKey)
+ err := environs.Bootstrap(env, constraints.Value{})
c.Assert(err, IsNil)
}
=== modified file 'environs/maas/environprovider.go'
--- environs/maas/environprovider.go 2013-04-04 12:30:22 +0000
+++ environs/maas/environprovider.go 2013-04-08 08:09:22 +0000
@@ -19,13 +19,24 @@
}
func (maasEnvironProvider) Open(cfg *config.Config) (environs.Environ, error) {
- log.Printf("environs/maas: opening environment %q.", cfg.Name())
+ log.Debugf("environs/maas: opening environment %q.", cfg.Name())
return NewEnviron(cfg)
}
// BoilerplateConfig is specified in the EnvironProvider interface.
func (maasEnvironProvider) BoilerplateConfig() string {
- panic("Not implemented.")
+ return `
+ maas:
+ type: maas
+ # Change this to where your MAAS server lives. It must specify the API endpoint.
+ maas-server: 'http://192.168.1.1/MAAS/api/1.0'
+ maas-oauth: '<add your OAuth credentials from MAAS here>'
+ admin-secret: {{rand}}
+ default-series: precise
+ authorized-keys-path: ~/.ssh/authorized_keys # or any file you want.
+ # Or:
+ # authorized-keys: ssh-rsa keymaterialhere
+`[1:]
}
// SecretAttrs is specified in the EnvironProvider interface.
=== modified file 'environs/maas/environprovider_test.go'
--- environs/maas/environprovider_test.go 2013-04-04 12:19:37 +0000
+++ environs/maas/environprovider_test.go 2013-04-08 08:09:22 +0000
@@ -15,12 +15,15 @@
var _ = Suite(new(EnvironProviderSuite))
func (suite *EnvironProviderSuite) TestSecretAttrsReturnsSensitiveMAASAttributes(c *C) {
+ testJujuHome := c.MkDir()
+ defer config.SetJujuHome(config.SetJujuHome(testJujuHome))
const oauth = "aa:bb:cc"
attrs := map[string]interface{}{
- "maas-oauth": oauth,
- "maas-server": "http://maas.example.com/maas/api/1.0/",
- "name": "wheee",
- "type": "maas",
+ "maas-oauth": oauth,
+ "maas-server": "http://maas.example.com/maas/api/1.0/",
+ "name": "wheee",
+ "type": "maas",
+ "authorized-keys": "I-am-not-a-real-key",
}
config, err := config.New(attrs)
c.Assert(err, IsNil)
=== modified file 'environs/maas/instance.go'
--- environs/maas/instance.go 2013-02-08 11:07:33 +0000
+++ environs/maas/instance.go 2013-04-08 08:09:22 +0000
@@ -4,6 +4,7 @@
"launchpad.net/gomaasapi"
"launchpad.net/juju-core/environs"
"launchpad.net/juju-core/state"
+ "launchpad.net/juju-core/state/api/params"
)
type maasInstance struct {
@@ -44,14 +45,14 @@
return instance.DNSName()
}
-func (instance *maasInstance) OpenPorts(machineId string, ports []state.Port) error {
- panic("Not implemented.")
-}
-
-func (instance *maasInstance) ClosePorts(machineId string, ports []state.Port) error {
- panic("Not implemented.")
-}
-
-func (instance *maasInstance) Ports(machineId string) ([]state.Port, error) {
+func (instance *maasInstance) OpenPorts(machineId string, ports []params.Port) error {
+ panic("Not implemented.")
+}
+
+func (instance *maasInstance) ClosePorts(machineId string, ports []params.Port) error {
+ panic("Not implemented.")
+}
+
+func (instance *maasInstance) Ports(machineId string) ([]params.Port, error) {
panic("Not implemented.")
}
=== modified file 'environs/maas/util_test.go'
--- environs/maas/util_test.go 2013-04-05 09:16:18 +0000
+++ environs/maas/util_test.go 2013-04-08 08:09:22 +0000
@@ -35,7 +35,8 @@
}
func (s *UtilSuite) TestUserData(c *C) {
-
+ testJujuHome := c.MkDir()
+ defer config.SetJujuHome(config.SetJujuHome(testJujuHome))
tools := &state.Tools{
URL: "http://foo.com/tools/juju1.2.3-linux-amd64.tgz",
Binary: version.MustParseBinary("1.2.3-linux-amd64"),
=== added file 'environs/mongo.go'
--- environs/mongo.go 1970-01-01 00:00:00 +0000
+++ environs/mongo.go 2013-04-08 08:09:22 +0000
@@ -0,0 +1,48 @@
+package environs
+
+import (
+ "fmt"
+)
+
+// MongoURL figures out from where to retrieve a copy of MongoDB compatible with
+// the given version from the given environment. The search locations are (in order):
+// - the environment specific storage
+// - the public storage
+// - a "well known" EC2 bucket
+func MongoURL(env Environ, series, architecture string) string {
+ path := MongoStoragePath(series, architecture)
+ url, err := findMongo(env.Storage(), path)
+ if err == nil {
+ return url
+ }
+ url, err = findMongo(env.PublicStorage(), path)
+ if err == nil {
+ return url
+ }
+ // TODO(thumper): this should at least check that the fallback option
+ // exists before returning it. lp:1164220
+ return fmt.Sprintf("http://juju-dist.s3.amazonaws.com/%s", path)
+}
+
+// Return the URL of a compatible MongoDB (if it exists) from the storage,
+// for the given series and architecture (in vers).
+func findMongo(store StorageReader, path string) (string, error) {
+ names, err := store.List(path)
+ if err != nil {
+ return "", err
+ }
+ if len(names) != 1 {
+ return "", &NotFoundError{fmt.Errorf("%s not found", path)}
+ }
+ url, err := store.URL(names[0])
+ if err != nil {
+ return "", err
+ }
+ return url, nil
+}
+
+// MongoStoragePath returns the path that is used to
+// retrieve the given version of mongodb in a Storage.
+func MongoStoragePath(series, architecture string) string {
+ return fmt.Sprintf("tools/mongo-2.2.0-%s-%s.tgz", series, architecture)
+}
=== added file 'environs/mongo_test.go'
--- environs/mongo_test.go 1970-01-01 00:00:00 +0000
+++ environs/mongo_test.go 2013-04-08 08:09:22 +0000
@@ -0,0 +1,87 @@
+package environs_test
+
+import (
+ . "launchpad.net/gocheck"
+ "launchpad.net/juju-core/environs"
+ "launchpad.net/juju-core/environs/dummy"
+ "launchpad.net/juju-core/testing"
+ "launchpad.net/juju-core/version"
+)
+
+type MongoToolsSuite struct {
+ env environs.Environ
+ testing.LoggingSuite
+ dataDir string
+}
+
+func (t *MongoToolsSuite) SetUpTest(c *C) {
+ t.LoggingSuite.SetUpTest(c)
+ env, err := environs.NewFromAttrs(map[string]interface{}{
+ "name": "test",
+ "type": "dummy",
+ "state-server": false,
+ "authorized-keys": "i-am-a-key",
+ "ca-cert": testing.CACert,
+ "ca-private-key": "",
+ })
+ c.Assert(err, IsNil)
+ t.env = env
+ t.dataDir = c.MkDir()
+}
+
+func (t *MongoToolsSuite) TearDownTest(c *C) {
+ dummy.Reset()
+ t.LoggingSuite.TearDownTest(c)
+}
+
+func currentMongoPath() string {
+ return environs.MongoStoragePath(version.CurrentSeries(), version.CurrentArch())
+}
+
+var mongoURLTests = []struct {
+ summary string // a summary of the test purpose.
+ contents []string // names in private storage.
+ publicContents []string // names in public storage.
+ expect string // the name we expect to find (if no error).
+ urlpart string // part of the url we expect to find (if not blank).
+}{{
+ summary: "grab mongo from private storage if it exists there",
+ contents: []string{currentMongoPath()},
+ expect: currentMongoPath(),
+}, {
+ summary: "fall back to public storage when nothing found in private",
+ contents: []string{
+ environs.MongoStoragePath("foo", version.CurrentArch()),
+ },
+ publicContents: []string{
+ currentMongoPath(),
+ },
+ expect: "public-" + currentMongoPath(),
+}, {
+ summary: "if nothing in public or private storage, fall back to copy in ec2",
+ contents: []string{
+ environs.MongoStoragePath("foo", version.CurrentArch()),
+ environs.MongoStoragePath(version.CurrentSeries(), "foo"),
+ },
+ publicContents: []string{
+ environs.MongoStoragePath("foo", version.CurrentArch()),
+ },
+ urlpart: "http://juju-dist.s3.amazonaws.com",
+},
+}
+
+func (t *MongoToolsSuite) TestMongoURL(c *C) {
+ for i, tt := range mongoURLTests {
+ c.Logf("Test %d: %s", i, tt.summary)
+ putNames(c, t.env, tt.contents, tt.publicContents)
+ mongoURL := environs.MongoURL(t.env, version.CurrentSeries(), version.CurrentArch())
+ if tt.expect != "" {
+ assertURLContents(c, mongoURL, tt.expect)
+ }
+ if tt.urlpart != "" {
+ c.Assert(mongoURL, Matches, tt.urlpart+".*")
+ }
+ t.env.Destroy(nil)
+ dummy.ResetPublicStorage(t.env)
+ }
+}
=== modified file 'environs/open_test.go'
--- environs/open_test.go 2012-11-27 17:27:45 +0000
+++ environs/open_test.go 2013-04-08 08:09:22 +0000
@@ -2,8 +2,9 @@
import (
. "launchpad.net/gocheck"
+ "launchpad.net/juju-core/constraints"
"launchpad.net/juju-core/environs"
- _ "launchpad.net/juju-core/environs/dummy"
+ "launchpad.net/juju-core/environs/dummy"
"launchpad.net/juju-core/testing"
)
@@ -11,6 +12,10 @@
var _ = Suite(&OpenSuite{})
+func (OpenSuite) TearDownTest(c *C) {
+ dummy.Reset()
+}
+
func (OpenSuite) TestNewDummyEnviron(c *C) {
// matches *Settings.Map()
config := map[string]interface{}{
@@ -24,7 +29,7 @@
}
env, err := environs.NewFromAttrs(config)
c.Assert(err, IsNil)
- c.Assert(env.Bootstrap(false, nil, nil), IsNil)
+ c.Assert(env.Bootstrap(constraints.Value{}, nil, nil), IsNil)
}
func (OpenSuite) TestNewUnknownEnviron(c *C) {
@@ -38,3 +43,18 @@
c.Assert(err, ErrorMatches, "no registered provider for.*")
c.Assert(env, IsNil)
}
+
+func (OpenSuite) TestNewFromNameNoDefault(c *C) {
+ defer testing.MakeFakeHome(c, testing.MultipleEnvConfigNoDefault, testing.SampleCertName).Restore()
+
+ _, err := environs.NewFromName("")
+ c.Assert(err, ErrorMatches, "no default environment found")
+}
+
+func (OpenSuite) TestNewFromNameGetDefault(c *C) {
+ defer testing.MakeFakeHome(c, testing.SingleEnvConfig, testing.SampleCertName).Restore()
+
+ e, err := environs.NewFromName("")
+ c.Assert(err, IsNil)
+ c.Assert(e.Name(), Equals, "erewhemos")
+}
=== modified file 'environs/openstack/config.go'
--- environs/openstack/config.go 2013-02-26 03:12:19 +0000
+++ environs/openstack/config.go 2013-04-08 08:09:22 +0000
@@ -35,7 +35,7 @@
"public-bucket-url": "",
"default-image-id": "",
"default-instance-type": "",
- "use-floating-ip": true,
+ "use-floating-ip": false,
},
)
=== modified file 'environs/openstack/config_test.go'
--- environs/openstack/config_test.go 2013-02-26 03:12:19 +0000
+++ environs/openstack/config_test.go 2013-04-08 08:09:22 +0000
@@ -9,7 +9,8 @@
)
type ConfigSuite struct {
- savedVars map[string]string
+ savedVars map[string]string
+ oldJujuHome string
}
// Ensure any environment variables a user may have set locally are reset.
@@ -41,16 +42,14 @@
pbucketURL string
imageId string
instanceType string
- // useFloatingIP is true by default.
- // bools default to false so invert the attribute
- internalIPOnly bool
- username string
- password string
- tenantName string
- authMode string
- authURL string
- firewallMode config.FirewallMode
- err string
+ useFloatingIP bool
+ username string
+ password string
+ tenantName string
+ authMode string
+ authURL string
+ firewallMode config.FirewallMode
+ err string
}
type attrs map[string]interface{}
@@ -59,7 +58,8 @@
envs := attrs{
"environments": attrs{
"testenv": attrs{
- "type": "openstack",
+ "type": "openstack",
+ "authorized-keys": "fakekey",
},
},
}
@@ -133,10 +133,11 @@
if t.instanceType != "" {
c.Assert(ecfg.defaultInstanceType(), Equals, t.instanceType)
}
- c.Assert(ecfg.useFloatingIP(), Equals, !t.internalIPOnly)
+ c.Assert(ecfg.useFloatingIP(), Equals, t.useFloatingIP)
}
func (s *ConfigSuite) SetUpTest(c *C) {
+ s.oldJujuHome = config.SetJujuHome(c.MkDir())
s.savedVars = make(map[string]string)
for v, val := range envVars {
s.savedVars[v] = os.Getenv(v)
@@ -148,6 +149,7 @@
for k, v := range s.savedVars {
os.Setenv(k, v)
}
+ config.SetJujuHome(s.oldJujuHome)
}
var configTests = []configTest{
@@ -254,14 +256,14 @@
instanceType: "instance-type",
}, {
summary: "default use floating ip",
- // Use floating IP's by default.
- internalIPOnly: false,
+ // Do not use floating IP's by default.
+ useFloatingIP: false,
}, {
summary: "use floating ip",
config: attrs{
- "use-floating-ip": false,
+ "use-floating-ip": true,
},
- internalIPOnly: true,
+ useFloatingIP: true,
}, {
summary: "public bucket URL",
config: attrs{
@@ -374,7 +376,8 @@
envs := attrs{
"environments": attrs{
"testenv": attrs{
- "type": "openstack",
+ "type": "openstack",
+ "authorized-keys": "fakekey",
},
},
}
@@ -399,7 +402,8 @@
envs := attrs{
"environments": attrs{
"testenv": attrs{
- "type": "openstack",
+ "type": "openstack",
+ "authorized-keys": "fakekey",
},
},
}
=== modified file 'environs/openstack/export_test.go'
--- environs/openstack/export_test.go 2013-03-01 04:36:01 +0000
+++ environs/openstack/export_test.go 2013-04-08 08:09:22 +0000
@@ -5,35 +5,54 @@
"launchpad.net/goose/nova"
"launchpad.net/goose/swift"
"launchpad.net/juju-core/environs"
+ "launchpad.net/juju-core/environs/jujutest"
"launchpad.net/juju-core/state"
"launchpad.net/juju-core/trivial"
"net/http"
+ "time"
)
+// This provides the content for code accessing test:///... URLs. This allows
+// us to set the responses for things like the Metadata server, by pointing
+// metadata requests at test:///... rather than http://169.254.169.254
+var testRoundTripper = &jujutest.ProxyRoundTripper{}
+
func init() {
- http.DefaultTransport.(*http.Transport).RegisterProtocol("file", http.NewFileTransport(http.Dir("testdata")))
+ http.DefaultTransport.(*http.Transport).RegisterProtocol("test", testRoundTripper)
}
var origMetadataHost = metadataHost
-func UseTestMetadata(local bool) {
- if local {
- metadataHost = "file:"
+var metadataContent = `{"uuid": "d8e02d56-2648-49a3-bf97-6be8f1204f38",` +
+ `"availability_zone": "nova", "hostname": "test.novalocal", ` +
+ `"launch_index": 0, "meta": {"priority": "low", "role": "webserver"}, ` +
+ `"public_keys": {"mykey": "ssh-rsa fake-key\n"}, "name": "test"}`
+
+// A group of canned responses for the "metadata server". These match
+// reasonably well with the results of making those requests on a Folsom+
+// Openstack service
+var MetadataTestingBase = []jujutest.FileContent{
+ {"/latest/meta-data/instance-id", "i-000abc"},
+ {"/latest/meta-data/local-ipv4", "10.1.1.2"},
+ {"/latest/meta-data/public-ipv4", "203.1.1.2"},
+ {"/openstack/2012-08-10/meta_data.json", metadataContent},
+}
+
+// This is the same as MetadataTestingBase, but it doesn't have the openstack
+// 2012-08-08 API. This matches what is available in HP Cloud.
+var MetadataHP = MetadataTestingBase[:len(MetadataTestingBase)-1]
+
+// Set Metadata requests to be served by the filecontent supplied.
+func UseTestMetadata(metadata []jujutest.FileContent) {
+ if len(metadata) != 0 {
+ testRoundTripper.Sub = jujutest.NewVirtualRoundTripper(metadata)
+ metadataHost = "test:"
} else {
+ testRoundTripper.Sub = nil
metadataHost = origMetadataHost
}
}
-var origMetadataJSON = metadataJSON
-
-func UseMetadataJSON(path string) {
- if path != "" {
- metadataJSON = path
- } else {
- metadataJSON = origMetadataJSON
- }
-}
-
var originalShortAttempt = shortAttempt
var originalLongAttempt = longAttempt
@@ -43,8 +62,8 @@
func ShortTimeouts(short bool) {
if short {
shortAttempt = trivial.AttemptStrategy{
- Total: 0.25e9,
- Delay: 0.01e9,
+ Total: 100 * time.Millisecond,
+ Delay: 10 * time.Millisecond,
}
longAttempt = shortAttempt
} else {
=== modified file 'environs/openstack/live_test.go'
--- environs/openstack/live_test.go 2013-02-27 10:24:35 +0000
+++ environs/openstack/live_test.go 2013-04-08 08:09:22 +0000
@@ -10,9 +10,8 @@
"launchpad.net/juju-core/environs"
"launchpad.net/juju-core/environs/jujutest"
"launchpad.net/juju-core/environs/openstack"
+ envtesting "launchpad.net/juju-core/environs/testing"
coretesting "launchpad.net/juju-core/testing"
- "launchpad.net/juju-core/version"
- "strings"
)
// generate a different bucket name for each config instance, so that
@@ -26,7 +25,7 @@
return fmt.Sprintf("%x", buf)
}
-func makeTestConfig() map[string]interface{} {
+func makeTestConfig(cred *identity.Credentials) map[string]interface{} {
// The following attributes hold the environment configuration
// for running the OpenStack integration tests.
//
@@ -36,22 +35,33 @@
// secret-key: $OS_PASSWORD
//
attrs := map[string]interface{}{
- "name": "sample-" + randomName(),
- "type": "openstack",
- "auth-mode": "userpass",
- "control-bucket": "juju-test-" + randomName(),
- "ca-cert": coretesting.CACert,
- "ca-private-key": coretesting.CAKey,
+ "name": "sample-" + randomName(),
+ "type": "openstack",
+ "auth-mode": "userpass",
+ "control-bucket": "juju-test-" + randomName(),
+ "ca-cert": coretesting.CACert,
+ "ca-private-key": coretesting.CAKey,
+ "authorized-keys": "fakekey",
+ "admin-secret": "secret",
+ "username": cred.User,
+ "password": cred.Secrets,
+ "region": cred.Region,
+ "auth-url": cred.URL,
+ "tenant-name": cred.TenantName,
}
return attrs
}
// Register tests to run against a real Openstack instance.
func registerLiveTests(cred *identity.Credentials, testImageDetails openstack.ImageDetails) {
+ config := makeTestConfig(cred)
+ config["default-image-id"] = testImageDetails.ImageId
+ config["default-instance-type"] = testImageDetails.Flavor
Suite(&LiveTests{
cred: cred,
LiveTests: jujutest.LiveTests{
- Attempt: *openstack.ShortAttempt,
+ TestConfig: jujutest.TestConfig{config},
+ Attempt: *openstack.ShortAttempt,
// TODO: Bug #1133263, once the infrastructure is set up,
// enable The state tests on openstack
CanOpenState: false,
@@ -79,29 +89,18 @@
func (t *LiveTests) SetUpSuite(c *C) {
t.LoggingSuite.SetUpSuite(c)
- // Get an authenticated Goose client to extract some configuration parameters for the test environment.
+ // Update some Config items now that we have services running.
+ // This is setting the public-bucket-url and auth-url because that
+ // information is set during startup of the localLiveSuite
cl := client.NewClient(t.cred, identity.AuthUserPass, nil)
err := cl.Authenticate()
c.Assert(err, IsNil)
publicBucketURL, err := cl.MakeServiceURL("object-store", nil)
c.Assert(err, IsNil)
- attrs := makeTestConfig()
- attrs["admin-secret"] = "secret"
- attrs["username"] = t.cred.User
- attrs["password"] = t.cred.Secrets
- attrs["region"] = t.cred.Region
- attrs["auth-url"] = t.cred.URL
- attrs["tenant-name"] = t.cred.TenantName
- attrs["public-bucket-url"] = publicBucketURL
- attrs["default-image-id"] = t.testImageId
- attrs["default-instance-type"] = t.testFlavor
- t.Config = attrs
- t.LiveTests = jujutest.LiveTests{
- Config: attrs,
- Attempt: *openstack.ShortAttempt,
- CanOpenState: false, // no state; local tests (unless -live is passed)
- HasProvisioner: false, // don't deploy anything
- }
+ t.TestConfig.UpdateConfig(map[string]interface{}{
+ "public-bucket-url": publicBucketURL,
+ "auth-url": t.cred.URL,
+ })
t.LiveTests.SetUpSuite(c)
// Environ.PublicStorage() is read only.
// For testing, we create a specific storage instance which is authorised to write to
@@ -110,7 +109,7 @@
// Put some fake tools in place so that tests that are simply
// starting instances without any need to check if those instances
// are running will find them in the public bucket.
- putFakeTools(c, t.writeablePublicStorage)
+ envtesting.PutFakeTools(c, t.writeablePublicStorage)
}
func (t *LiveTests) TearDownSuite(c *C) {
@@ -136,18 +135,6 @@
t.LoggingSuite.TearDownTest(c)
}
-// putFakeTools sets up a bucket containing something
-// that looks like a tools archive so test methods
-// that start an instance can succeed even though they
-// do not upload tools.
-func putFakeTools(c *C, s environs.StorageWriter) {
- path := environs.ToolsStoragePath(version.Current)
- c.Logf("putting fake tools at %v", path)
- toolsContents := "tools archive, honest guv"
- err := s.Put(path, strings.NewReader(toolsContents), int64(len(toolsContents)))
- c.Assert(err, IsNil)
-}
-
func (t *LiveTests) TestFindImageSpec(c *C) {
instanceType := openstack.DefaultInstanceType(t.Env)
imageId, flavorId, err := openstack.FindInstanceSpec(t.Env, "precise", "amd64", instanceType)
=== modified file 'environs/openstack/local_test.go'
--- environs/openstack/local_test.go 2013-03-05 19:41:32 +0000
+++ environs/openstack/local_test.go 2013-04-08 08:09:22 +0000
@@ -6,12 +6,15 @@
"launchpad.net/goose/identity"
"launchpad.net/goose/testservices/hook"
"launchpad.net/goose/testservices/openstackservice"
+ "launchpad.net/juju-core/constraints"
"launchpad.net/juju-core/environs"
"launchpad.net/juju-core/environs/jujutest"
"launchpad.net/juju-core/environs/openstack"
+ envtesting "launchpad.net/juju-core/environs/testing"
"launchpad.net/juju-core/juju/testing"
"launchpad.net/juju-core/state"
coretesting "launchpad.net/juju-core/testing"
+ "launchpad.net/juju-core/version"
"net/http"
"net/http/httptest"
)
@@ -20,9 +23,17 @@
var _ = Suite(&ProviderSuite{})
+func (s *ProviderSuite) SetUpTest(c *C) {
+ openstack.ShortTimeouts(true)
+}
+
+func (s *ProviderSuite) TearDownTest(c *C) {
+ openstack.ShortTimeouts(false)
+}
+
func (s *ProviderSuite) TestMetadata(c *C) {
- openstack.UseTestMetadata(true)
- defer openstack.UseTestMetadata(false)
+ openstack.UseTestMetadata(openstack.MetadataTestingBase)
+ defer openstack.UseTestMetadata(nil)
p, err := environs.Provider("openstack")
c.Assert(err, IsNil)
@@ -40,13 +51,31 @@
c.Assert(id, Equals, state.InstanceId("d8e02d56-2648-49a3-bf97-6be8f1204f38"))
}
+func (s *ProviderSuite) TestPublicFallbackToPrivate(c *C) {
+ openstack.UseTestMetadata([]jujutest.FileContent{
+ {"/latest/meta-data/public-ipv4", "203.1.1.2"},
+ {"/latest/meta-data/local-ipv4", "10.1.1.2"},
+ })
+ defer openstack.UseTestMetadata(nil)
+ p, err := environs.Provider("openstack")
+ c.Assert(err, IsNil)
+
+ addr, err := p.PublicAddress()
+ c.Assert(err, IsNil)
+ c.Assert(addr, Equals, "203.1.1.2")
+
+ openstack.UseTestMetadata([]jujutest.FileContent{
+ {"/latest/meta-data/local-ipv4", "10.1.1.2"},
+ {"/latest/meta-data/public-ipv4", ""},
+ })
+ addr, err = p.PublicAddress()
+ c.Assert(err, IsNil)
+ c.Assert(addr, Equals, "10.1.1.2")
+}
+
func (s *ProviderSuite) TestLegacyInstanceId(c *C) {
- openstack.UseTestMetadata(true)
- openstack.UseMetadataJSON("invalid.json")
- defer func() {
- openstack.UseTestMetadata(false)
- openstack.UseMetadataJSON("")
- }()
+ openstack.UseTestMetadata(openstack.MetadataHP)
+ defer openstack.UseTestMetadata(nil)
p, err := environs.Provider("openstack")
c.Assert(err, IsNil)
@@ -64,13 +93,25 @@
Region: "some region",
TenantName: "some tenant",
}
+ config := makeTestConfig(cred)
+ config["authorized-keys"] = "fakekey"
+ config["default-image-id"] = "1"
+ config["default-instance-type"] = "m1.small"
Suite(&localLiveSuite{
LiveTests: LiveTests{
- cred: cred,
+ cred: cred,
+ testImageId: "1",
+ testFlavor: "m1.small",
+ LiveTests: jujutest.LiveTests{
+ TestConfig: jujutest.TestConfig{config},
+ },
},
})
Suite(&localServerSuite{
cred: cred,
+ Tests: jujutest.Tests{
+ TestConfig: jujutest.TestConfig{config},
+ },
})
}
@@ -89,6 +130,7 @@
s.Mux = http.NewServeMux()
s.Server.Config.Handler = s.Mux
cred.URL = s.Server.URL
+ c.Logf("Started service at: %v", s.Server.URL)
s.Service = openstackservice.New(cred)
s.Service.SetupHTTP(s.Mux)
openstack.ShortTimeouts(true)
@@ -108,6 +150,29 @@
srv localServer
}
+func (s *localLiveSuite) SetUpSuite(c *C) {
+ s.LoggingSuite.SetUpSuite(c)
+ c.Logf("Running live tests using openstack service test double")
+ s.srv.start(c, s.cred)
+ s.LiveTests.SetUpSuite(c)
+}
+
+func (s *localLiveSuite) TearDownSuite(c *C) {
+ s.LiveTests.TearDownSuite(c)
+ s.srv.stop()
+ s.LoggingSuite.TearDownSuite(c)
+}
+
+func (s *localLiveSuite) SetUpTest(c *C) {
+ s.LoggingSuite.SetUpTest(c)
+ s.LiveTests.SetUpTest(c)
+}
+
+func (s *localLiveSuite) TearDownTest(c *C) {
+ s.LiveTests.TearDownTest(c)
+ s.LoggingSuite.TearDownTest(c)
+}
+
// localServerSuite contains tests that run against an Openstack service double.
// These tests can test things that would be unreasonably slow or expensive
// to test on a live Openstack server. The service double is started and stopped for
@@ -120,32 +185,6 @@
env environs.Environ
}
-func (s *localLiveSuite) SetUpSuite(c *C) {
- s.LoggingSuite.SetUpSuite(c)
- c.Logf("Running live tests using openstack service test double")
-
- s.testImageId = "1"
- s.testFlavor = "m1.small"
- s.srv.start(c, s.cred)
- s.LiveTests.SetUpSuite(c)
-}
-
-func (s *localLiveSuite) TearDownSuite(c *C) {
- s.LiveTests.TearDownSuite(c)
- s.srv.stop()
- s.LoggingSuite.TearDownSuite(c)
-}
-
-func (s *localLiveSuite) SetUpTest(c *C) {
- s.LoggingSuite.SetUpTest(c)
- s.LiveTests.SetUpTest(c)
-}
-
-func (s *localLiveSuite) TearDownTest(c *C) {
- s.LiveTests.TearDownTest(c)
- s.LoggingSuite.TearDownTest(c)
-}
-
func (s *localServerSuite) SetUpSuite(c *C) {
s.LoggingSuite.SetUpSuite(c)
s.Tests.SetUpSuite(c)
@@ -157,28 +196,15 @@
s.LoggingSuite.TearDownSuite(c)
}
-func testConfig(cred *identity.Credentials) map[string]interface{} {
- attrs := makeTestConfig()
- attrs["admin-secret"] = "secret"
- attrs["username"] = cred.User
- attrs["password"] = cred.Secrets
- attrs["region"] = cred.Region
- attrs["auth-url"] = cred.URL
- attrs["tenant-name"] = cred.TenantName
- attrs["default-image-id"] = "1"
- attrs["default-instance-type"] = "m1.small"
- return attrs
-}
-
func (s *localServerSuite) SetUpTest(c *C) {
s.LoggingSuite.SetUpTest(c)
s.srv.start(c, s.cred)
- s.Tests = jujutest.Tests{
- Config: testConfig(s.cred),
- }
+ s.TestConfig.UpdateConfig(map[string]interface{}{
+ "auth-url": s.cred.URL,
+ })
s.Tests.SetUpTest(c)
writeablePublicStorage := openstack.WritablePublicStorage(s.Env)
- putFakeTools(c, writeablePublicStorage)
+ envtesting.PutFakeTools(c, writeablePublicStorage)
s.env = s.Tests.Env
}
@@ -188,13 +214,9 @@
s.LoggingSuite.TearDownTest(c)
}
-func panicWrite(name string, cert, key []byte) error {
- panic("writeCertAndKey called unexpectedly")
-}
-
-// If the bootstrap node is configured to require a public IP address (the default),
+// If the bootstrap node is configured to require a public IP address,
// bootstrapping fails if an address cannot be allocated.
-func (s *localLiveSuite) TestBootstrapFailsWhenPublicIPError(c *C) {
+func (s *localServerSuite) TestBootstrapFailsWhenPublicIPError(c *C) {
cleanup := s.srv.Service.Nova.RegisterControlPoint(
"addFloatingIP",
func(sc hook.ServiceControl, args ...interface{}) error {
@@ -202,8 +224,15 @@
},
)
defer cleanup()
- err := environs.Bootstrap(s.Env, true, panicWrite)
- c.Assert(err, ErrorMatches, ".*cannot allocate a public IP as needed.*")
+ // Create a config that matches s.Config but with use-floating-ip set to true
+ s.TestConfig.UpdateConfig(map[string]interface{}{
+ "use-floating-ip": true,
+ })
+ // TODO: Just share jujutest.Tests.Open rather than accessing .Config
+ env, err := environs.NewFromAttrs(s.TestConfig.Config)
+ c.Assert(err, IsNil)
+ err = environs.Bootstrap(env, constraints.Value{})
+ c.Assert(err, ErrorMatches, "(.|\n)*cannot allocate a public IP as needed(.|\n)*")
}
// If the environment is configured not to require a public IP address for nodes,
@@ -224,10 +253,9 @@
},
)
defer cleanup()
- err := environs.Bootstrap(s.Env, true, panicWrite)
- c.Assert(err, IsNil)
- inst, err := s.Env.StartInstance("100", testing.InvalidStateInfo("100"), testing.InvalidAPIInfo("100"), nil)
- c.Assert(err, IsNil)
+ err := environs.Bootstrap(s.Env, constraints.Value{})
+ c.Assert(err, IsNil)
+ inst := testing.StartInstance(c, s.Env, "100")
err = s.Env.StopInstances([]environs.Instance{inst})
c.Assert(err, IsNil)
}
@@ -280,11 +308,9 @@
}
func (s *localServerSuite) TestInstancesGathering(c *C) {
- inst0, err := s.Env.StartInstance("100", testing.InvalidStateInfo("100"), testing.InvalidAPIInfo("100"), nil)
- c.Assert(err, IsNil)
+ inst0 := testing.StartInstance(c, s.Env, "100")
id0 := inst0.Id()
- inst1, err := s.Env.StartInstance("101", testing.InvalidStateInfo("101"), testing.InvalidAPIInfo("101"), nil)
- c.Assert(err, IsNil)
+ inst1 := testing.StartInstance(c, s.Env, "101")
id1 := inst1.Id()
defer func() {
err := s.Env.StopInstances([]environs.Instance{inst0, inst1})
@@ -323,9 +349,9 @@
// It should be moved to environs.jujutests.Tests.
func (t *localServerSuite) TestBootstrapInstanceUserDataAndState(c *C) {
policy := t.env.AssignmentPolicy()
- c.Assert(policy, Equals, state.AssignUnused)
+ c.Assert(policy, Equals, state.AssignNew)
- err := environs.Bootstrap(t.env, true, panicWrite)
+ err := environs.Bootstrap(t.env, constraints.Value{})
c.Assert(err, IsNil)
// check that the state holds the id of the bootstrap machine.
@@ -352,9 +378,10 @@
// check that a new instance will be started with a machine agent,
// and without a provisioning agent.
- info.EntityName = "machine-1"
- apiInfo.EntityName = "machine-1"
- inst1, err := t.env.StartInstance("1", info, apiInfo, nil)
+ series := version.Current.Series
+ info.Tag = "machine-1"
+ apiInfo.Tag = "machine-1"
+ inst1, err := t.env.StartInstance("1", series, constraints.Value{}, info, apiInfo)
c.Assert(err, IsNil)
err = t.env.Destroy(append(insts, inst1))
=== modified file 'environs/openstack/provider.go'
--- environs/openstack/provider.go 2013-03-05 19:41:32 +0000
+++ environs/openstack/provider.go 2013-04-08 08:09:22 +0000
@@ -12,12 +12,14 @@
"launchpad.net/goose/identity"
"launchpad.net/goose/nova"
"launchpad.net/goose/swift"
+ "launchpad.net/juju-core/constraints"
"launchpad.net/juju-core/environs"
"launchpad.net/juju-core/environs/cloudinit"
"launchpad.net/juju-core/environs/config"
"launchpad.net/juju-core/log"
"launchpad.net/juju-core/state"
"launchpad.net/juju-core/state/api"
+ "launchpad.net/juju-core/state/api/params"
"launchpad.net/juju-core/trivial"
"launchpad.net/juju-core/version"
"net/http"
@@ -124,7 +126,7 @@
}
func (p environProvider) Open(cfg *config.Config) (environs.Environ, error) {
- log.Printf("environs/openstack: opening environment %q", cfg.Name())
+ log.Infof("environs/openstack: opening environment %q", cfg.Name())
e := new(environ)
err := e.SetConfig(cfg)
if err != nil {
@@ -146,7 +148,12 @@
}
func (p environProvider) PublicAddress() (string, error) {
- return fetchMetadata("public-ipv4")
+ if addr, err := fetchMetadata("public-ipv4"); err != nil {
+ return "", err
+ } else if addr != "" {
+ return addr, nil
+ }
+ return p.PrivateAddress()
}
func (p environProvider) PrivateAddress() (string, error) {
@@ -166,10 +173,6 @@
// server when needed.
var metadataHost = "http://169.254.169.254"
-// metadataJSON holds the path of the instance's JSON metadata.
-// It is a variable so that tests can change it when needed.
-var metadataJSON = "2012-08-10/meta-data.json"
-
// fetchMetadata fetches a single atom of data from the openstack instance metadata service.
// http://docs.amazonwebservices.com/AWSEC2/latest/UserGuide/AESDG-chapter-instancedata.html
// (the same specs is implemented in ec2, hence the reference)
@@ -186,7 +189,7 @@
// the same thing as the "instance-id" in the ec2-style metadata. This only
// works on openstack Folsom or later.
func fetchInstanceUUID() (string, error) {
- uri := fmt.Sprintf("%s/%s", metadataHost, metadataJSON)
+ uri := fmt.Sprintf("%s/openstack/2012-08-10/meta_data.json", metadataHost)
data, err := retryGet(uri)
if err != nil {
return "", err
@@ -346,7 +349,7 @@
// TODO: following 30 lines nearly verbatim from environs/ec2
-func (inst *instance) OpenPorts(machineId string, ports []state.Port) error {
+func (inst *instance) OpenPorts(machineId string, ports []params.Port) error {
if inst.e.Config().FirewallMode() != config.FwInstance {
return fmt.Errorf("invalid firewall mode for opening ports on instance: %q",
inst.e.Config().FirewallMode())
@@ -355,11 +358,11 @@
if err := inst.e.openPortsInGroup(name, ports); err != nil {
return err
}
- log.Printf("environs/openstack: opened ports in security group %s: %v", name, ports)
+ log.Infof("environs/openstack: opened ports in security group %s: %v", name, ports)
return nil
}
-func (inst *instance) ClosePorts(machineId string, ports []state.Port) error {
+func (inst *instance) ClosePorts(machineId string, ports []params.Port) error {
if inst.e.Config().FirewallMode() != config.FwInstance {
return fmt.Errorf("invalid firewall mode for closing ports on instance: %q",
inst.e.Config().FirewallMode())
@@ -368,11 +371,11 @@
if err := inst.e.closePortsInGroup(name, ports); err != nil {
return err
}
- log.Printf("environs/openstack: closed ports in security group %s: %v", name, ports)
+ log.Infof("environs/openstack: closed ports in security group %s: %v", name, ports)
return nil
}
-func (inst *instance) Ports(machineId string) ([]state.Port, error) {
+func (inst *instance) Ports(machineId string) ([]params.Port, error) {
if inst.e.Config().FirewallMode() != config.FwInstance {
return nil, fmt.Errorf("invalid firewall mode for retrieving ports from instance: %q",
inst.e.Config().FirewallMode())
@@ -415,12 +418,23 @@
return e.publicStorageUnlocked
}
-func (e *environ) Bootstrap(uploadTools bool, cert, key []byte) error {
+// TODO(thumper): this code is duplicated in ec2 and openstack. Ideally we
+// should refactor the tools selection criteria with the version that is in
+// environs. The constraints work will require this refactoring.
+func findTools(env *environ) (*state.Tools, error) {
+ flags := environs.HighestVersion | environs.CompatVersion
+ v := version.Current
+ v.Series = env.Config().DefaultSeries()
+ // TODO: set Arch based on constraints (when they are landed)
+ return environs.FindTools(env, v, flags)
+}
+
+func (e *environ) Bootstrap(cons constraints.Value, cert, key []byte) error {
password := e.Config().AdminSecret()
if password == "" {
return fmt.Errorf("admin-secret is required for bootstrap")
}
- log.Printf("environs/openstack: bootstrapping environment %q", e.name)
+ log.Infof("environs/openstack: bootstrapping environment %q", e.name)
// If the state file exists, it might actually have just been
// removed by Destroy, and eventual consistency has not caught
// up yet, so we retry to verify if that is happening.
@@ -437,21 +451,12 @@
if _, notFound := err.(*environs.NotFoundError); !notFound {
return fmt.Errorf("cannot query old bootstrap state: %v", err)
}
- var tools *state.Tools
- if uploadTools {
- tools, err = environs.PutTools(e.Storage(), nil)
- if err != nil {
- return fmt.Errorf("cannot upload tools: %v", err)
- }
- } else {
- flags := environs.HighestVersion | environs.CompatVersion
- v := version.Current
- v.Series = e.Config().DefaultSeries()
- tools, err = environs.FindTools(e, v, flags)
- if err != nil {
- return fmt.Errorf("cannot find tools: %v", err)
- }
+
+ tools, err := findTools(e)
+ if err != nil {
+ return fmt.Errorf("cannot find tools: %v", err)
}
+
config, err := environs.BootstrapConfig(providerInstance, e.Config(), tools)
if err != nil {
return fmt.Errorf("unable to determine inital configuration: %v", err)
@@ -460,12 +465,10 @@
if !hasCert {
return fmt.Errorf("no CA certificate in environment configuration")
}
- v := version.Current
- v.Series = tools.Series
- v.Arch = tools.Arch
- mongoURL := environs.MongoURL(e, v)
+ mongoURL := environs.MongoURL(e, tools.Series, tools.Arch)
inst, err := e.startInstance(&startInstanceParams{
machineId: "0",
+ series: tools.Series,
info: &state.Info{
Password: trivial.PasswordHash(password),
CACert: caCert,
@@ -478,6 +481,7 @@
mongoURL: mongoURL,
stateServer: true,
config: config,
+ constraints: cons,
stateServerCert: cert,
stateServerKey: key,
withPublicIP: e.ecfg().useFloatingIP(),
@@ -516,7 +520,7 @@
var apiAddrs []string
// Wait for the DNS names of any of the instances
// to become available.
- log.Printf("environs/openstack: waiting for DNS name(s) of state server instances %v", st.StateInstances)
+ log.Infof("environs/openstack: waiting for DNS name(s) of state server instances %v", st.StateInstances)
for a := longAttempt.Start(); len(stateAddrs) == 0 && a.Next(); {
insts, err := e.Instances(st.StateInstances)
if err != nil && err != environs.ErrPartialInstances {
@@ -625,24 +629,27 @@
return nil
}
-func (e *environ) StartInstance(machineId string, info *state.Info, apiInfo *api.Info, tools *state.Tools) (environs.Instance, error) {
+func (e *environ) StartInstance(machineId string, series string, cons constraints.Value, info *state.Info, apiInfo *api.Info) (environs.Instance, error) {
return e.startInstance(&startInstanceParams{
machineId: machineId,
+ series: series,
+ constraints: cons,
info: info,
apiInfo: apiInfo,
- tools: tools,
withPublicIP: e.ecfg().useFloatingIP(),
})
}
type startInstanceParams struct {
machineId string
+ series string
info *state.Info
apiInfo *api.Info
tools *state.Tools
mongoURL string
stateServer bool
config *config.Config
+ constraints constraints.Value
stateServerCert []byte
stateServerKey []byte
@@ -666,6 +673,7 @@
MachineId: scfg.machineId,
AuthorizedKeys: e.ecfg().AuthorizedKeys(),
Config: scfg.config,
+ Constraints: scfg.constraints,
}
cloudcfg, err := cloudinit.New(cfg)
if err != nil {
@@ -739,21 +747,26 @@
return nil, fmt.Errorf("cannot allocate a public IP as needed: %v", err)
} else {
publicIP = fip
- log.Printf("environs/openstack: allocated public IP %s", publicIP.IP)
+ log.Infof("environs/openstack: allocated public IP %s", publicIP.IP)
}
}
+ // TODO(fwereade): use scfg.constraints to pick instance spec before
+ // settling on tools.
if scfg.tools == nil {
var err error
flags := environs.HighestVersion | environs.CompatVersion
- scfg.tools, err = environs.FindTools(e, version.Current, flags)
+ v := version.Current
+ v.Series = scfg.series
+ scfg.tools, err = environs.FindTools(e, v, flags)
if err != nil {
return nil, err
}
}
- log.Printf("environs/openstack: starting machine %s in %q running tools version %q from %q",
+ log.Infof("environs/openstack: starting machine %s in %q running tools version %q from %q",
scfg.machineId, e.name, scfg.tools.Binary, scfg.tools.URL)
- if strings.Contains(scfg.tools.Series, "unknown") || strings.Contains(scfg.tools.Arch, "unknown") {
- return nil, fmt.Errorf("cannot find image for unknown series or architecture")
+ if strings.Contains(scfg.tools.Series, "unknown") {
+ // TODO(fwereade): this is somewhat crazy.
+ return nil, fmt.Errorf("cannot find image for %q", scfg.tools.Series)
}
spec, err := findInstanceSpec(e, &instanceConstraint{
series: scfg.tools.Series,
@@ -799,7 +812,7 @@
return nil, fmt.Errorf("cannot get started instance: %v", err)
}
inst := &instance{e, detail, ""}
- log.Printf("environs/openstack: started instance %q", inst.Id())
+ log.Infof("environs/openstack: started instance %q", inst.Id())
if scfg.withPublicIP {
if err := e.assignPublicIP(publicIP, string(inst.Id())); err != nil {
if err := e.terminateInstances([]state.InstanceId{inst.Id()}); err != nil {
@@ -808,7 +821,7 @@
}
return nil, fmt.Errorf("cannot assign public address %s to instance %q: %v", publicIP.IP, inst.Id(), err)
}
- log.Printf("environs/openstack: assigned public IP %s to %q", publicIP.IP, inst.Id())
+ log.Infof("environs/openstack: assigned public IP %s to %q", publicIP.IP, inst.Id())
}
return inst, nil
}
@@ -906,7 +919,7 @@
}
func (e *environ) Destroy(ensureInsts []environs.Instance) error {
- log.Printf("environs/openstack: destroying environment %q", e.name)
+ log.Infof("environs/openstack: destroying environment %q", e.name)
insts, err := e.AllInstances()
if err != nil {
return fmt.Errorf("cannot get instances: %v", err)
@@ -940,7 +953,12 @@
}
func (e *environ) AssignmentPolicy() state.AssignmentPolicy {
- return state.AssignUnused
+ // Until we get proper containers to install units into, we shouldn't
+ // reuse dirty machines, as we cannot guarantee that when units were
+ // removed, it was left in a clean state. Once we have good
+ // containerisation for the units, we should be able to have the ability
+ // to assign back to unused machines.
+ return state.AssignNew
}
func (e *environ) globalGroupName() string {
@@ -956,7 +974,7 @@
}
func (e *environ) machineFullName(machineId string) string {
- return fmt.Sprintf("juju-%s-%s", e.Name(), state.MachineEntityName(machineId))
+ return fmt.Sprintf("juju-%s-%s", e.Name(), state.MachineTag(machineId))
}
// machinesFilter returns a nova.Filter matching all machines in the environment.
@@ -966,7 +984,7 @@
return filter
}
-func (e *environ) openPortsInGroup(name string, ports []state.Port) error {
+func (e *environ) openPortsInGroup(name string, ports []params.Port) error {
novaclient := e.nova()
group, err := novaclient.SecurityGroupByName(name)
if err != nil {
@@ -988,7 +1006,7 @@
return nil
}
-func (e *environ) closePortsInGroup(name string, ports []state.Port) error {
+func (e *environ) closePortsInGroup(name string, ports []params.Port) error {
if len(ports) == 0 {
return nil
}
@@ -1015,14 +1033,14 @@
return nil
}
-func (e *environ) portsInGroup(name string) (ports []state.Port, err error) {
+func (e *environ) portsInGroup(name string) (ports []params.Port, err error) {
group, err := e.nova().SecurityGroupByName(name)
if err != nil {
return nil, err
}
for _, p := range (*group).Rules {
for i := *p.FromPort; i <= *p.ToPort; i++ {
- ports = append(ports, state.Port{
+ ports = append(ports, params.Port{
Protocol: *p.IPProtocol,
Number: i,
})
@@ -1034,7 +1052,7 @@
// TODO: following 30 lines nearly verbatim from environs/ec2
-func (e *environ) OpenPorts(ports []state.Port) error {
+func (e *environ) OpenPorts(ports []params.Port) error {
if e.Config().FirewallMode() != config.FwGlobal {
return fmt.Errorf("invalid firewall mode for opening ports on environment: %q",
e.Config().FirewallMode())
@@ -1042,11 +1060,11 @@
if err := e.openPortsInGroup(e.globalGroupName(), ports); err != nil {
return err
}
- log.Printf("environs/openstack: opened ports in global group: %v", ports)
+ log.Infof("environs/openstack: opened ports in global group: %v", ports)
return nil
}
-func (e *environ) ClosePorts(ports []state.Port) error {
+func (e *environ) ClosePorts(ports []params.Port) error {
if e.Config().FirewallMode() != config.FwGlobal {
return fmt.Errorf("invalid firewall mode for closing ports on environment: %q",
e.Config().FirewallMode())
@@ -1054,11 +1072,11 @@
if err := e.closePortsInGroup(e.globalGroupName(), ports); err != nil {
return err
}
- log.Printf("environs/openstack: closed ports in global group: %v", ports)
+ log.Infof("environs/openstack: closed ports in global group: %v", ports)
return nil
}
-func (e *environ) Ports() ([]state.Port, error) {
+func (e *environ) Ports() ([]params.Port, error) {
if e.Config().FirewallMode() != config.FwGlobal {
return nil, fmt.Errorf("invalid firewall mode for retrieving ports from environment: %q",
e.Config().FirewallMode())
=== modified file 'environs/openstack/storage.go'
--- environs/openstack/storage.go 2012-12-21 06:56:22 +0000
+++ environs/openstack/storage.go 2013-04-08 08:09:22 +0000
@@ -51,8 +51,8 @@
func (s *storage) Get(file string) (r io.ReadCloser, err error) {
for a := shortAttempt.Start(); a.Next(); {
r, err = s.swift.GetReader(s.containerName, file)
- if errors.IsNotFound(err) {
- continue
+ if !errors.IsNotFound(err) {
+ break
}
}
err, _ = maybeNotFound(err)
=== removed directory 'environs/openstack/testdata'
=== removed directory 'environs/openstack/testdata/2012-08-10'
=== removed file 'environs/openstack/testdata/2012-08-10/meta-data.json'
--- environs/openstack/testdata/2012-08-10/meta-data.json 2013-02-27 13:28:43 +0000
+++ environs/openstack/testdata/2012-08-10/meta-data.json 1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
-{"uuid": "d8e02d56-2648-49a3-bf97-6be8f1204f38", "availability_zone": "nova", "hostname": "test.novalocal", "launch_index": 0, "meta": {"priority": "low", "role": "webserver"}, "public_keys": {"mykey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDYVEprvtYJXVOBN0XNKVVRNCRX6BlnNbI+USLGais1sUWPwtSg7z9K9vhbYAPUZcq8c/s5S9dg5vTHbsiyPCIDOKyeHba4MUJq8Oh5b2i71/3BISpyxTBH/uZDHdslW2a+SrPDCeuMMoss9NFhBdKtDkdG9zyi0ibmCP6yMdEX8Q== Generated by Nova\n"}, "name": "test"}
=== removed directory 'environs/openstack/testdata/latest'
=== removed directory 'environs/openstack/testdata/latest/meta-data'
=== removed file 'environs/openstack/testdata/latest/meta-data/instance-id'
--- environs/openstack/testdata/latest/meta-data/instance-id 2013-03-01 04:36:01 +0000
+++ environs/openstack/testdata/latest/meta-data/instance-id 1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
-i-000abc
\ No newline at end of file
=== removed file 'environs/openstack/testdata/latest/meta-data/local-ipv4'
--- environs/openstack/testdata/latest/meta-data/local-ipv4 2013-03-05 19:41:32 +0000
+++ environs/openstack/testdata/latest/meta-data/local-ipv4 1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
-10.1.1.2
=== removed file 'environs/openstack/testdata/latest/meta-data/public-ipv4'
--- environs/openstack/testdata/latest/meta-data/public-ipv4 2013-03-05 19:41:32 +0000
+++ environs/openstack/testdata/latest/meta-data/public-ipv4 1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
-203.1.1.2
=== added directory 'environs/testing'
=== added file 'environs/testing/tools.go'
--- environs/testing/tools.go 1970-01-01 00:00:00 +0000
+++ environs/testing/tools.go 2013-04-08 08:09:22 +0000
@@ -0,0 +1,29 @@
+package testing
+
+import (
+ . "launchpad.net/gocheck"
+ "launchpad.net/juju-core/environs"
+ "launchpad.net/juju-core/environs/config"
+ "launchpad.net/juju-core/version"
+ "strings"
+)
+
+// PutFakeTools sets up a bucket containing something
+// that looks like a tools archive so test methods
+// that start an instance can succeed even though they
+// do not upload tools.
+func PutFakeTools(c *C, s environs.StorageWriter) {
+ toolsVersion := version.Current
+ path := environs.ToolsStoragePath(toolsVersion)
+ c.Logf("putting fake tools at %v", path)
+ toolsContents := "tools archive, honest guv"
+ err := s.Put(path, strings.NewReader(toolsContents), int64(len(toolsContents)))
+ c.Assert(err, IsNil)
+ if toolsVersion.Series != config.DefaultSeries {
+ toolsVersion.Series = config.DefaultSeries
+ path = environs.ToolsStoragePath(toolsVersion)
+ c.Logf("putting fake tools at %v", path)
+ err = s.Put(path, strings.NewReader(toolsContents), int64(len(toolsContents)))
+ c.Assert(err, IsNil)
+ }
+}
=== modified file 'environs/tools.go'
--- environs/tools.go 2013-02-24 21:51:05 +0000
+++ environs/tools.go 2013-04-08 08:09:22 +0000
@@ -15,7 +15,7 @@
"strings"
)
-var toolPrefix = "tools/juju-"
+const toolPrefix = "tools/juju-"
// ToolsList holds a list of available tools. Private tools take
// precedence over public tools, even if they have a lower
@@ -47,30 +47,33 @@
// a particular storage.
func listTools(store StorageReader, majorVersion int) ([]*state.Tools, error) {
dir := fmt.Sprintf("%s%d.", toolPrefix, majorVersion)
+ log.Debugf("listing tools in dir: %s", dir)
names, err := store.List(dir)
if err != nil {
return nil, err
}
var toolsList []*state.Tools
for _, name := range names {
+ log.Debugf("looking at tools file %s", name)
if !strings.HasPrefix(name, toolPrefix) || !strings.HasSuffix(name, ".tgz") {
- log.Printf("environs: unexpected tools file found %q", name)
+ log.Warningf("environs: unexpected tools file found %q", name)
continue
}
vers := name[len(toolPrefix) : len(name)-len(".tgz")]
var t state.Tools
t.Binary, err = version.ParseBinary(vers)
if err != nil {
- log.Printf("environs: failed to parse %q: %v", vers, err)
+ log.Warningf("environs: failed to parse %q: %v", vers, err)
continue
}
if t.Major != majorVersion {
- log.Printf("environs: tool %q found in wrong directory %q", name, dir)
+ log.Warningf("environs: tool %q found in wrong directory %q", name, dir)
continue
}
t.URL, err = store.URL(name)
+ log.Debugf("tools URL is %s", t.URL)
if err != nil {
- log.Printf("environs: cannot get URL for %q: %v", name, err)
+ log.Warningf("environs: cannot get URL for %q: %v", name, err)
continue
}
toolsList = append(toolsList, &t)
@@ -78,11 +81,14 @@
return toolsList, nil
}
-// PutTools builds the current version of the juju tools, uploads them
-// to the given storage, and returns a Tools instance describing them.
-// If forceVersion is not nil, the uploaded tools bundle will report
-// the given version number.
-func PutTools(storage Storage, forceVersion *version.Number) (*state.Tools, error) {
+// PutTools builds whatever version of launchpad.net/juju-core is in $GOPATH,
+// uploads it to the given storage, and returns a Tools instance describing
+// them. If forceVersion is not nil, the uploaded tools bundle will report
+// the given version number; if any fakeSeries are supplied, additional copies
+// of the built tools will be uploaded for use by machines of those series.
+// Juju tools built for one series do not necessarily run on another, but this
+// func exists only for development use cases.
+func PutTools(storage Storage, forceVersion *version.Number, fakeSeries ...string) (*state.Tools, error) {
// TODO(rog) find binaries from $PATH when not using a development
// version of juju