← Back to team overview

launchpad-reviewers team mailing list archive

[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