diff -Nru juju-core-1.17.6/debian/changelog juju-core-1.17.7/debian/changelog --- juju-core-1.17.6/debian/changelog 2014-03-24 16:05:49.000000000 +0000 +++ juju-core-1.17.7/debian/changelog 2014-03-28 08:58:44.000000000 +0000 @@ -1,3 +1,11 @@ +juju-core (1.17.7-0ubuntu1) trusty; urgency=medium + + * New upstream point release, including fixes for: + - no debug log with all providers on Ubuntu 14.04 (LP: #1294776). + * d/control: Add cpu-checker dependency to juju-local (LP: #1297077). + + -- James Page Fri, 28 Mar 2014 08:58:42 +0000 + juju-core (1.17.6-0ubuntu1) trusty; urgency=medium * New upstream point release, including fixes for: diff -Nru juju-core-1.17.6/debian/control juju-core-1.17.7/debian/control --- juju-core-1.17.6/debian/control 2014-03-21 11:49:45.000000000 +0000 +++ juju-core-1.17.7/debian/control 2014-03-27 21:19:45.000000000 +0000 @@ -40,6 +40,7 @@ Package: juju-local Architecture: all Depends: + cpu-checker, juju-core (>= ${source:Version}), juju-mongodb, lxc, diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/charm/charm.go juju-core-1.17.7/src/launchpad.net/juju-core/charm/charm.go --- juju-core-1.17.6/src/launchpad.net/juju-core/charm/charm.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/charm/charm.go 2014-03-27 15:48:42.000000000 +0000 @@ -40,7 +40,7 @@ if localRepoPath == "" { return nil, errors.New("path to local repository not specified") } - repo = &LocalRepository{localRepoPath} + repo = &LocalRepository{Path: localRepoPath} default: return nil, fmt.Errorf("unknown schema for charm URL %q", curl) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/charm/repo.go juju-core-1.17.7/src/launchpad.net/juju-core/charm/repo.go --- juju-core-1.17.6/src/launchpad.net/juju-core/charm/repo.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/charm/repo.go 2014-03-27 15:48:42.000000000 +0000 @@ -24,11 +24,12 @@ // InfoResponse is sent by the charm store in response to charm-info requests. type InfoResponse struct { - Revision int `json:"revision"` // Zero is valid. Can't omitempty. - Sha256 string `json:"sha256,omitempty"` - Digest string `json:"digest,omitempty"` - Errors []string `json:"errors,omitempty"` - Warnings []string `json:"warnings,omitempty"` + CanonicalURL string `json:"canonical-url,omitempty"` + Revision int `json:"revision"` // Zero is valid. Can't omitempty. + Sha256 string `json:"sha256,omitempty"` + Digest string `json:"digest,omitempty"` + Errors []string `json:"errors,omitempty"` + Warnings []string `json:"warnings,omitempty"` } // EventResponse is sent by the charm store in response to charm-event requests. @@ -53,6 +54,7 @@ type Repository interface { Get(curl *URL) (Charm, error) Latest(curls ...*URL) ([]CharmRevision, error) + Resolve(ref Reference) (*URL, error) } // Latest returns the latest revision of the charm referenced by curl, regardless @@ -137,8 +139,27 @@ return http.DefaultClient.Do(req) } +// Resolve canonicalizes charm URLs, resolving references and implied series. +func (s *CharmStore) Resolve(ref Reference) (*URL, error) { + infos, err := s.Info(ref) + if err != nil { + return nil, err + } + if len(infos) == 0 { + return nil, fmt.Errorf("missing response when resolving charm URL: %q", ref) + } + if infos[0].CanonicalURL == "" { + return nil, fmt.Errorf("cannot resolve charm URL: %q", ref) + } + curl, err := ParseURL(infos[0].CanonicalURL) + if err != nil { + return nil, err + } + return curl, nil +} + // Info returns details for all the specified charms in the charm store. -func (s *CharmStore) Info(curls ...*URL) ([]*InfoResponse, error) { +func (s *CharmStore) Info(curls ...Location) ([]*InfoResponse, error) { baseURL := s.BaseURL + "/charm-info?" queryParams := make([]string, len(curls), len(curls)+1) for i, curl := range curls { @@ -218,7 +239,7 @@ } // revisions returns the revisions of the charms referenced by curls. -func (s *CharmStore) revisions(curls ...*URL) (revisions []CharmRevision, err error) { +func (s *CharmStore) revisions(curls ...Location) (revisions []CharmRevision, err error) { infos, err := s.Info(curls...) if err != nil { return nil, err @@ -246,7 +267,7 @@ // Latest returns the latest revision of the charms referenced by curls, regardless // of the revision set on each curl. func (s *CharmStore) Latest(curls ...*URL) ([]CharmRevision, error) { - baseCurls := make([]*URL, len(curls)) + baseCurls := make([]Location, len(curls)) for i, curl := range curls { baseCurls[i] = curl.WithRevision(-1) } @@ -387,11 +408,27 @@ // /path/to/repository/precise/mongodb.charm // /path/to/repository/precise/wordpress/ type LocalRepository struct { - Path string + Path string + defaultSeries string } var _ Repository = (*LocalRepository)(nil) +// WithDefaultSeries returns a Repository with the default series set. +func (r *LocalRepository) WithDefaultSeries(defaultSeries string) Repository { + localRepo := *r + localRepo.defaultSeries = defaultSeries + return &localRepo +} + +// Resolve canonicalizes charm URLs, resolving references and implied series. +func (r *LocalRepository) Resolve(ref Reference) (*URL, error) { + if r.defaultSeries == "" { + return nil, fmt.Errorf("cannot resolve, repository has no default series: %q", ref) + } + return &URL{Reference: ref, Series: r.defaultSeries}, nil +} + // Latest returns the latest revision of the charm referenced by curl, regardless // of the revision set on curl itself. func (r *LocalRepository) Latest(curls ...*URL) ([]CharmRevision, error) { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/charm/repo_test.go juju-core-1.17.7/src/launchpad.net/juju-core/charm/repo_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/charm/repo_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/charm/repo_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -173,7 +173,7 @@ // The following tests cover the low-level CharmStore-specific API. func (s *StoreSuite) TestInfo(c *gc.C) { - charmURLs := []*charm.URL{ + charmURLs := []charm.Location{ charm.MustParseURL("cs:series/good"), charm.MustParseURL("cs:series/better"), charm.MustParseURL("cs:series/best"), @@ -387,7 +387,7 @@ func (s *LocalRepoSuite) SetUpTest(c *gc.C) { s.LoggingSuite.SetUpTest(c) root := c.MkDir() - s.repo = &charm.LocalRepository{root} + s.repo = &charm.LocalRepository{Path: root} s.seriesPath = filepath.Join(root, "quantal") c.Assert(os.Mkdir(s.seriesPath, 0777), gc.IsNil) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/charm/testing/mockstore.go juju-core-1.17.7/src/launchpad.net/juju-core/charm/testing/mockstore.go --- juju-core-1.17.6/src/launchpad.net/juju-core/charm/testing/mockstore.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/charm/testing/mockstore.go 2014-03-27 15:48:42.000000000 +0000 @@ -6,6 +6,7 @@ import ( "bytes" "encoding/json" + "fmt" "io" "net" "net/http" @@ -35,13 +36,14 @@ Metadata []string InfoRequestCount int InfoRequestCountNoStats int + DefaultSeries string charms map[string]int } // NewMockStore creates a mock charm store containing the specified charms. func NewMockStore(c *gc.C, charms map[string]int) *MockStore { - s := &MockStore{charms: charms} + s := &MockStore{charms: charms, DefaultSeries: "precise"} f, err := os.Open(testing.Charms.BundlePath(c.MkDir(), "dummy")) c.Assert(err, gc.IsNil) defer f.Close() @@ -98,7 +100,17 @@ for _, url := range r.Form["charms"] { cr := &charm.InfoResponse{} response[url] = cr - charmURL := charm.MustParseURL(url) + charmURL, err := charm.ParseURL(url) + if err == charm.ErrUnresolvedUrl { + ref, _, err := charm.ParseReference(url) + if err != nil { + panic(err) + } + if s.DefaultSeries == "" { + panic(fmt.Errorf("mock store lacks a default series cannot resolve charm URL: %q", url)) + } + charmURL = &charm.URL{Reference: ref, Series: s.DefaultSeries} + } switch charmURL.Name { case "borken": cr.Errors = append(cr.Errors, "badness") @@ -115,6 +127,7 @@ cr.Revision = charmURL.Revision } cr.Sha256 = s.bundleSha256 + cr.CanonicalURL = charmURL.String() } else { cr.Errors = append(cr.Errors, "entry not found") } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/charm/url.go juju-core-1.17.7/src/launchpad.net/juju-core/charm/url.go --- juju-core-1.17.6/src/launchpad.net/juju-core/charm/url.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/charm/url.go 2014-03-27 15:48:42.000000000 +0000 @@ -13,23 +13,42 @@ "labix.org/v2/mgo/bson" ) -// A charm URL represents charm locations such as: +// Location represents a charm location, which must declare a path component +// and a string representaion. +type Location interface { + Path() string + String() string +} + +// Reference represents a charm location with an unresolved, untargeted series, +// such as: +// +// cs:~joe/wordpress +// cs:wordpress-42 +type Reference struct { + Schema string // "cs" or "local" + User string // "joe" + Name string // "wordpress" + Revision int // -1 if unset, N otherwise +} + +// URL represents a fully resolved charm location with a specific series, such +// as: // // cs:~joe/oneiric/wordpress // cs:oneiric/wordpress-42 // local:oneiric/wordpress // type URL struct { - Schema string // "cs" or "local" - User string // "joe" - Series string // "oneiric" - Name string // "wordpress" - Revision int // -1 if unset, N otherwise + Reference + Series string // "oneiric" } +var ErrUnresolvedUrl error = fmt.Errorf("charm url series is not resolved") + var ( validUser = regexp.MustCompile("^[a-z0-9][a-zA-Z0-9+.-]+$") - validSeries = regexp.MustCompile("^[a-z]+([a-z-]+[a-z])?$") + validSeries = regexp.MustCompile("^[a-z]+([a-z0-9]+)?$") validName = regexp.MustCompile("^[a-z][a-z0-9]*(-[a-z0-9]*[a-z][a-z0-9]*)*$") ) @@ -68,67 +87,83 @@ // ParseURL parses the provided charm URL string into its respective // structure. func ParseURL(url string) (*URL, error) { - u := &URL{} + r, series, err := ParseReference(url) + if err != nil { + return nil, err + } + if series == "" { + return nil, ErrUnresolvedUrl + } + return &URL{Reference: r, Series: series}, nil +} + +// ParseReference parses the provided charm Reference string into its +// respective structure and the targeted series, if present. +func ParseReference(url string) (Reference, string, error) { + r := Reference{Schema: "cs"} + series := "" i := strings.Index(url, ":") - if i > 0 { - u.Schema = url[:i] + if i >= 0 { + r.Schema = url[:i] i++ + } else { + i = 0 } // cs: or local: - if u.Schema != "cs" && u.Schema != "local" { - return nil, fmt.Errorf("charm URL has invalid schema: %q", url) + if r.Schema != "cs" && r.Schema != "local" { + return Reference{}, "", fmt.Errorf("charm URL has invalid schema: %q", url) } parts := strings.Split(url[i:], "/") if len(parts) < 1 || len(parts) > 3 { - return nil, fmt.Errorf("charm URL has invalid form: %q", url) + return Reference{}, "", fmt.Errorf("charm URL has invalid form: %q", url) } // ~ if strings.HasPrefix(parts[0], "~") { - if u.Schema == "local" { - return nil, fmt.Errorf("local charm URL with user name: %q", url) + if r.Schema == "local" { + return Reference{}, "", fmt.Errorf("local charm URL with user name: %q", url) } - u.User = parts[0][1:] - if !IsValidUser(u.User) { - return nil, fmt.Errorf("charm URL has invalid user name: %q", url) + r.User = parts[0][1:] + if !IsValidUser(r.User) { + return Reference{}, "", fmt.Errorf("charm URL has invalid user name: %q", url) } parts = parts[1:] } // - if len(parts) < 2 { - return nil, fmt.Errorf("charm URL without series: %q", url) - } if len(parts) == 2 { - u.Series = parts[0] - if !IsValidSeries(u.Series) { - return nil, fmt.Errorf("charm URL has invalid series: %q", url) + series = parts[0] + if !IsValidSeries(series) { + return Reference{}, "", fmt.Errorf("charm URL has invalid series: %q", url) } parts = parts[1:] } + if len(parts) < 1 { + return Reference{}, "", fmt.Errorf("charm URL without charm name: %q", url) + } // [-] - u.Name = parts[0] - u.Revision = -1 - for i := len(u.Name) - 1; i > 0; i-- { - c := u.Name[i] + r.Name = parts[0] + r.Revision = -1 + for i := len(r.Name) - 1; i > 0; i-- { + c := r.Name[i] if c >= '0' && c <= '9' { continue } - if c == '-' && i != len(u.Name)-1 { + if c == '-' && i != len(r.Name)-1 { var err error - u.Revision, err = strconv.Atoi(u.Name[i+1:]) + r.Revision, err = strconv.Atoi(r.Name[i+1:]) if err != nil { panic(err) // We just checked it was right. } - u.Name = u.Name[:i] + r.Name = r.Name[:i] } break } - if !IsValidName(u.Name) { - return nil, fmt.Errorf("charm URL has invalid charm name: %q", url) + if !IsValidName(r.Name) { + return Reference{}, "", fmt.Errorf("charm URL has invalid charm name: %q", url) } - return u, nil + return r, series, nil } // InferURL returns a charm URL inferred from src. The provided @@ -148,9 +183,12 @@ // when src does not include that information; similarly, a missing // schema is assumed to be 'cs'. func InferURL(src, defaultSeries string) (*URL, error) { - if u, err := ParseURL(src); err == nil { - // src was a valid charm URL already - return u, nil + r, series, err := ParseReference(src) + if err != nil { + return nil, err + } + if series != "" { + return &URL{Reference: r, Series: series}, nil } if strings.HasPrefix(src, "~") { return nil, fmt.Errorf("cannot infer charm URL with user but no schema: %q", src) @@ -187,22 +225,37 @@ } func (u *URL) Path() string { - if u.User != "" { - if u.Revision >= 0 { - return fmt.Sprintf("~%s/%s/%s-%d", u.User, u.Series, u.Name, u.Revision) - } - return fmt.Sprintf("~%s/%s/%s", u.User, u.Series, u.Name) + return u.path(u.Series) +} + +func (r Reference) path(series string) string { + var parts []string + if r.User != "" { + parts = append(parts, fmt.Sprintf("~%s", r.User)) } - if u.Revision >= 0 { - return fmt.Sprintf("%s/%s-%d", u.Series, u.Name, u.Revision) + if series != "" { + parts = append(parts, series) } - return fmt.Sprintf("%s/%s", u.Series, u.Name) + if r.Revision >= 0 { + parts = append(parts, fmt.Sprintf("%s-%d", r.Name, r.Revision)) + } else { + parts = append(parts, r.Name) + } + return strings.Join(parts, "/") +} + +func (r Reference) Path() string { + return r.path("") } func (u *URL) String() string { return fmt.Sprintf("%s:%s", u.Schema, u.Path()) } +func (r Reference) String() string { + return fmt.Sprintf("%s:%s", r.Schema, r.Path()) +} + // GetBSON turns u into a bson.Getter so it can be saved directly // on a MongoDB database with mgo. func (u *URL) GetBSON() (interface{}, error) { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/charm/url_test.go juju-core-1.17.7/src/launchpad.net/juju-core/charm/url_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/charm/url_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/charm/url_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" + "strings" "labix.org/v2/mgo/bson" gc "launchpad.net/gocheck" @@ -21,38 +22,63 @@ s, err string url *charm.URL }{ - {"cs:~user/series/name", "", &charm.URL{"cs", "user", "series", "name", -1}}, - {"cs:~user/series/name-0", "", &charm.URL{"cs", "user", "series", "name", 0}}, - {"cs:series/name", "", &charm.URL{"cs", "", "series", "name", -1}}, - {"cs:series/name-42", "", &charm.URL{"cs", "", "series", "name", 42}}, - {"local:series/name-1", "", &charm.URL{"local", "", "series", "name", 1}}, - {"local:series/name", "", &charm.URL{"local", "", "series", "name", -1}}, - {"local:series/n0-0n-n0", "", &charm.URL{"local", "", "series", "n0-0n-n0", -1}}, + {"cs:~user/series/name", "", &charm.URL{charm.Reference{"cs", "user", "name", -1}, "series"}}, + {"cs:~user/series/name-0", "", &charm.URL{charm.Reference{"cs", "user", "name", 0}, "series"}}, + {"cs:series/name", "", &charm.URL{charm.Reference{"cs", "", "name", -1}, "series"}}, + {"cs:series/name-42", "", &charm.URL{charm.Reference{"cs", "", "name", 42}, "series"}}, + {"local:series/name-1", "", &charm.URL{charm.Reference{"local", "", "name", 1}, "series"}}, + {"local:series/name", "", &charm.URL{charm.Reference{"local", "", "name", -1}, "series"}}, + {"local:series/n0-0n-n0", "", &charm.URL{charm.Reference{"local", "", "n0-0n-n0", -1}, "series"}}, + {"cs:~user/name", "", &charm.URL{charm.Reference{"cs", "user", "name", -1}, ""}}, + {"cs:name", "", &charm.URL{charm.Reference{"cs", "", "name", -1}, ""}}, + {"local:name", "", &charm.URL{charm.Reference{"local", "", "name", -1}, ""}}, {"bs:~user/series/name-1", "charm URL has invalid schema: .*", nil}, {"cs:~1/series/name-1", "charm URL has invalid user name: .*", nil}, + {"cs:~user", "charm URL without charm name: .*", nil}, {"cs:~user/1/name-1", "charm URL has invalid series: .*", nil}, {"cs:~user/series/name-1-2", "charm URL has invalid charm name: .*", nil}, {"cs:~user/series/name-1-name-2", "charm URL has invalid charm name: .*", nil}, {"cs:~user/series/name--name-2", "charm URL has invalid charm name: .*", nil}, {"cs:~user/series/huh/name-1", "charm URL has invalid form: .*", nil}, - {"cs:~user/name", "charm URL without series: .*", nil}, - {"cs:name", "charm URL without series: .*", nil}, + {"cs:/name", "charm URL has invalid series: .*", nil}, {"local:~user/series/name", "local charm URL with user name: .*", nil}, {"local:~user/name", "local charm URL with user name: .*", nil}, - {"local:name", "charm URL without series: .*", nil}, } func (s *URLSuite) TestParseURL(c *gc.C) { for i, t := range urlTests { c.Logf("test %d", i) - url, err := charm.ParseURL(t.s) + url, uerr := charm.ParseURL(t.s) + ref, series, rerr := charm.ParseReference(t.s) comment := gc.Commentf("ParseURL(%q)", t.s) - if t.err != "" { - c.Check(err.Error(), gc.Matches, t.err, comment) + if t.url != nil && t.url.Series == "" { + if t.err != "" { + // Expected error should match + c.Assert(rerr, gc.NotNil, comment) + c.Check(rerr.Error(), gc.Matches, t.err, comment) + } else { + // Expected charm reference should match + c.Check(ref, gc.DeepEquals, t.url.Reference, comment) + c.Check(t.url.Reference.String(), gc.Equals, t.s) + } + if rerr != nil { + // If ParseReference has an error, ParseURL should share it + c.Check(uerr.Error(), gc.Equals, rerr.Error(), comment) + } else { + // Otherwise, ParseURL with an empty series should error unresolved. + c.Check(uerr.Error(), gc.Equals, charm.ErrUnresolvedUrl.Error(), comment) + } } else { - c.Check(url, gc.DeepEquals, t.url, comment) - c.Check(t.url.String(), gc.Equals, t.s) + if t.err != "" { + c.Assert(uerr, gc.NotNil, comment) + c.Check(uerr.Error(), gc.Matches, t.err, comment) + c.Check(uerr.Error(), gc.Equals, rerr.Error(), comment) + } else { + c.Check(url.Series, gc.Equals, series, comment) + c.Check(url, gc.DeepEquals, t.url, comment) + c.Check(t.url.String(), gc.Equals, t.s) + } } } } @@ -82,31 +108,35 @@ comment := gc.Commentf("InferURL(%q, %q)", t.vague, "defseries") inferred, ierr := charm.InferURL(t.vague, "defseries") parsed, perr := charm.ParseURL(t.exact) - if parsed != nil { + if perr == nil { c.Check(inferred, gc.DeepEquals, parsed, comment) + c.Check(ierr, gc.IsNil) } else { expect := perr.Error() if t.vague != t.exact { - expect = fmt.Sprintf("%s (URL inferred from %q)", expect, t.vague) + if colIdx := strings.Index(expect, ":"); colIdx > 0 { + expect = expect[:colIdx] + } } - c.Check(ierr.Error(), gc.Equals, expect, comment) + c.Check(ierr.Error(), gc.Matches, expect+".*", comment) } } u, err := charm.InferURL("~blah", "defseries") c.Assert(u, gc.IsNil) - c.Assert(err, gc.ErrorMatches, "cannot infer charm URL with user but no schema: .*") + c.Assert(err, gc.ErrorMatches, "charm URL without charm name: .*") } var inferNoDefaultSeriesTests = []struct { vague, exact string + resolved bool }{ - {"foo", ""}, - {"foo-1", ""}, - {"cs:foo", ""}, - {"cs:~user/foo", ""}, - {"series/foo", "cs:series/foo"}, - {"cs:series/foo", "cs:series/foo"}, - {"cs:~user/series/foo", "cs:~user/series/foo"}, + {"foo", "", false}, + {"foo-1", "", false}, + {"cs:foo", "", false}, + {"cs:~user/foo", "", false}, + {"series/foo", "cs:series/foo", true}, + {"cs:series/foo", "cs:series/foo", true}, + {"cs:~user/series/foo", "cs:~user/series/foo", true}, } func (s *URLSuite) TestInferURLNoDefaultSeries(c *gc.C) { @@ -122,6 +152,23 @@ } } +func (s *URLSuite) TestParseUnresolved(c *gc.C) { + for _, t := range inferNoDefaultSeriesTests { + if t.resolved { + url, err := charm.ParseURL(t.vague) + c.Assert(err, gc.IsNil) + c.Assert(url.Series, gc.Not(gc.Equals), "") + } else { + _, series, err := charm.ParseReference(t.vague) + c.Assert(err, gc.IsNil) + c.Assert(series, gc.Equals, "") + _, err = charm.ParseURL(t.vague) + c.Assert(err, gc.NotNil) + c.Assert(err, gc.Equals, charm.ErrUnresolvedUrl) + } + } +} + var validTests = []struct { valid func(string) bool string string @@ -158,33 +205,42 @@ {charm.IsValidSeries, "precise", true}, {charm.IsValidSeries, "Precise", false}, {charm.IsValidSeries, "pre cise", false}, - {charm.IsValidSeries, "pre-cise", true}, + {charm.IsValidSeries, "pre-cise", false}, {charm.IsValidSeries, "pre^cise", false}, - {charm.IsValidSeries, "prec1se", false}, + {charm.IsValidSeries, "prec1se", true}, {charm.IsValidSeries, "-precise", false}, {charm.IsValidSeries, "precise-", false}, + {charm.IsValidSeries, "precise-1", false}, + {charm.IsValidSeries, "precise1", true}, {charm.IsValidSeries, "pre-c1se", false}, } func (s *URLSuite) TestValidCheckers(c *gc.C) { for i, t := range validTests { c.Logf("test %d: %s", i, t.string) - c.Assert(t.valid(t.string), gc.Equals, t.expect) + c.Assert(t.valid(t.string), gc.Equals, t.expect, gc.Commentf("%s", t.string)) } } func (s *URLSuite) TestMustParseURL(c *gc.C) { url := charm.MustParseURL("cs:series/name") - c.Assert(url, gc.DeepEquals, &charm.URL{"cs", "", "series", "name", -1}) - f := func() { charm.MustParseURL("local:name") } - c.Assert(f, gc.PanicMatches, "charm URL without series: .*") + c.Assert(url, gc.DeepEquals, + &charm.URL{Reference: charm.Reference{"cs", "", "name", -1}, Series: "series"}) + f := func() { charm.MustParseURL("local:@@/name") } + c.Assert(f, gc.PanicMatches, "charm URL has invalid series: .*") + f = func() { charm.MustParseURL("cs:~user") } + c.Assert(f, gc.PanicMatches, "charm URL without charm name: .*") + f = func() { charm.MustParseURL("cs:~user") } + c.Assert(f, gc.PanicMatches, "charm URL without charm name: .*") + f = func() { charm.MustParseURL("cs:name") } + c.Assert(f, gc.PanicMatches, "charm url series is not resolved") } func (s *URLSuite) TestWithRevision(c *gc.C) { url := charm.MustParseURL("cs:series/name") other := url.WithRevision(1) - c.Assert(url, gc.DeepEquals, &charm.URL{"cs", "", "series", "name", -1}) - c.Assert(other, gc.DeepEquals, &charm.URL{"cs", "", "series", "name", 1}) + c.Assert(url, gc.DeepEquals, &charm.URL{charm.Reference{"cs", "", "name", -1}, "series"}) + c.Assert(other, gc.DeepEquals, &charm.URL{charm.Reference{"cs", "", "name", 1}, "series"}) // Should always copy. The opposite behavior is error prone. c.Assert(other.WithRevision(1), gc.Not(gc.Equals), other) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cloudinit/sshinit/configure_test.go juju-core-1.17.7/src/launchpad.net/juju-core/cloudinit/sshinit/configure_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cloudinit/sshinit/configure_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cloudinit/sshinit/configure_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -55,7 +55,8 @@ func (s *configureSuite) getCloudConfig(c *gc.C, stateServer bool, vers version.Binary) *cloudinit.Config { var mcfg *envcloudinit.MachineConfig if stateServer { - mcfg = environs.NewBootstrapMachineConfig("http://whatever/dotcom", "private-key") + mcfg = environs.NewBootstrapMachineConfig("private-key") + mcfg.InstanceId = "instance-id" mcfg.Jobs = []params.MachineJob{params.JobManageEnviron, params.JobHostUnits} } else { mcfg = environs.NewMachineConfig("0", "ya", nil, nil) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/charm-admin/main.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/charm-admin/main.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/charm-admin/main.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/charm-admin/main.go 2014-03-27 15:48:42.000000000 +0000 @@ -4,12 +4,19 @@ package main import ( + "fmt" "os" "launchpad.net/juju-core/cmd" ) func main() { + ctx, err := cmd.DefaultContext() + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(2) + } + admcmd := cmd.NewSuperCommand(cmd.SuperCommandParams{ Name: "charm-admin", Log: &cmd.Log{}, @@ -17,5 +24,5 @@ admcmd.Register(&DeleteCharmCommand{}) - os.Exit(cmd.Main(admcmd, cmd.DefaultContext(), os.Args[1:])) + os.Exit(cmd.Main(admcmd, ctx, os.Args[1:])) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/cmd.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/cmd.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/cmd.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/cmd.go 2014-03-27 15:48:42.000000000 +0000 @@ -9,7 +9,6 @@ "fmt" "io" "io/ioutil" - "net/http" "os" "os/signal" "path/filepath" @@ -38,12 +37,6 @@ return &rcPassthroughError{code} } -func init() { - // Don't replace the default transport as other init blocks - // register protocols. - http.DefaultTransport.(*http.Transport).DisableKeepAlives = true -} - // ErrSilent can be returned from Run to signal that Main should exit with // code 1 without producing error output. var ErrSilent = errors.New("cmd: error out silently") @@ -97,10 +90,40 @@ // should interpret file names relative to Dir (see AbsPath below), and print // output and errors to Stdout and Stderr respectively. type Context struct { - Dir string - Stdin io.Reader - Stdout io.Writer - Stderr io.Writer + Dir string + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer + quiet bool + verbose bool +} + +func (ctx *Context) write(format string, params ...interface{}) { + output := fmt.Sprintf(format, params...) + if !strings.HasSuffix(output, "\n") { + output = output + "\n" + } + fmt.Fprint(ctx.Stderr, output) +} + +// Infof will write the formatted string to Stderr if quiet is false, but if +// quiet is true the message is logged. +func (ctx *Context) Infof(format string, params ...interface{}) { + if ctx.quiet { + logger.Infof(format, params...) + } else { + ctx.write(format, params...) + } +} + +// Verbosef will write the formatted string to Stderr if the verbose is true, +// and to the logger if not. +func (ctx *Context) Verbosef(format string, params ...interface{}) { + if ctx.verbose { + ctx.write(format, params...) + } else { + logger.Infof(format, params...) + } } // AbsPath returns an absolute representation of path, with relative paths @@ -231,21 +254,21 @@ } // DefaultContext returns a Context suitable for use in non-hosted situations. -func DefaultContext() *Context { +func DefaultContext() (*Context, error) { dir, err := os.Getwd() if err != nil { - panic(err) + return nil, err } abs, err := filepath.Abs(dir) if err != nil { - panic(err) + return nil, err } return &Context{ Dir: abs, Stdin: os.Stdin, Stdout: os.Stdout, Stderr: os.Stderr, - } + }, nil } // CheckEmpty is a utility function that returns an error if args is not empty. diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/cmd_test.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/cmd_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/cmd_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/cmd_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -6,6 +6,7 @@ import ( "bytes" "net/http" + "os" "path/filepath" jc "github.com/juju/testing/checkers" @@ -13,6 +14,7 @@ "launchpad.net/juju-core/cmd" "launchpad.net/juju-core/testing" + "launchpad.net/juju-core/utils" ) type CmdSuite struct{} @@ -22,6 +24,8 @@ func (s *CmdSuite) TestHttpTransport(c *gc.C) { transport := http.DefaultTransport.(*http.Transport) c.Assert(transport.DisableKeepAlives, jc.IsTrue) + client := utils.GetNonValidatingHTTPClient() + c.Assert(client.Transport.(*http.Transport).DisableKeepAlives, jc.IsTrue) } func (s *CmdSuite) TestContext(c *gc.C) { @@ -111,6 +115,23 @@ } } +func (s *CmdSuite) TestDefaultContextReturnsErrorInDeletedDirectory(c *gc.C) { + ctx := testing.Context(c) + wd, err := os.Getwd() + c.Assert(err, gc.IsNil) + missing := ctx.Dir + "/missing" + err = os.Mkdir(missing, 0700) + c.Assert(err, gc.IsNil) + err = os.Chdir(missing) + c.Assert(err, gc.IsNil) + defer os.Chdir(wd) + err = os.Remove(missing) + c.Assert(err, gc.IsNil) + ctx, err = cmd.DefaultContext() + c.Assert(err, gc.ErrorMatches, `getwd: no such file or directory`) + c.Assert(ctx, gc.IsNil) +} + func (s *CmdSuite) TestCheckEmpty(c *gc.C) { c.Assert(cmd.CheckEmpty(nil), gc.IsNil) c.Assert(cmd.CheckEmpty([]string{"boo!"}), gc.ErrorMatches, `unrecognized args: \["boo!"\]`) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/bootstrap.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/bootstrap.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/bootstrap.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/bootstrap.go 2014-03-27 15:48:42.000000000 +0000 @@ -100,6 +100,19 @@ if err := bootstrap.EnsureNotBootstrapped(environ); err != nil { return err } + + // Block interruption during bootstrap. Providers may also + // register for interrupt notification so they can exit early. + interrupted := make(chan os.Signal, 1) + defer close(interrupted) + ctx.InterruptNotify(interrupted) + defer ctx.StopInterruptNotify(interrupted) + go func() { + for _ = range interrupted { + ctx.Infof("Interrupt signalled: waiting for bootstrap to exit") + } + }() + // If --metadata-source is specified, override the default tools metadata source so // SyncTools can use it, and also upload any image metadata. if c.MetadataSource != "" { @@ -122,7 +135,7 @@ c.UploadTools = true } if c.UploadTools { - err = bootstrap.UploadTools(environ, c.Constraints.Arch, true, c.Series...) + err = bootstrap.UploadTools(ctx, environ, c.Constraints.Arch, true, c.Series...) if err != nil { return err } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/bootstrap_test.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/bootstrap_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/bootstrap_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/bootstrap_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -141,7 +141,7 @@ restore := envtools.TestingPatchBootstrapFindTools(mockFindTools) defer restore() - _, errc := runCommand(nullContext(), new(BootstrapCommand), test.args...) + _, errc := runCommand(nullContext(c), new(BootstrapCommand), test.args...) err := <-errc c.Check(findToolsRetryValues, gc.DeepEquals, test.expectedAllowRetry) stripped := strings.Replace(err.Error(), "\n", "", -1) @@ -222,7 +222,7 @@ } // Run command and check for uploads. - opc, errc := runCommand(nullContext(), new(BootstrapCommand), test.args...) + opc, errc := runCommand(nullContext(c), new(BootstrapCommand), test.args...) // Check for remaining operations/errors. if test.err != "" { err := <-errc @@ -292,8 +292,8 @@ err: `invalid value "bad=wrong" for flag --constraints: unknown constraint "bad"`, }, { info: "bad --series", - args: []string{"--series", "bad1"}, - err: `invalid value "bad1" for flag --series: invalid series name "bad1"`, + args: []string{"--series", "1bad1"}, + err: `invalid value "1bad1" for flag --series: invalid series name "1bad1"`, }, { info: "lonely --series", args: []string{"--series", "fine"}, @@ -374,7 +374,9 @@ ctx2 := coretesting.Context(c) code2 := cmd.Main(&BootstrapCommand{}, ctx2, nil) c.Check(code2, gc.Equals, 1) - c.Check(coretesting.Stderr(ctx2), gc.Equals, "error: environment is already bootstrapped\n") + expectedErrText := "Bootstrap failed, destroying environment\n" + expectedErrText += "error: environment is already bootstrapped\n" + c.Check(coretesting.Stderr(ctx2), gc.Equals, expectedErrText) c.Check(coretesting.Stdout(ctx2), gc.Equals, "") } @@ -507,7 +509,7 @@ } env := s.setupAutoUploadTest(c, "1.7.3", otherSeries) // Run command and check for that upload has been run for tools matching the current juju version. - opc, errc := runCommand(nullContext(), new(BootstrapCommand)) + opc, errc := runCommand(nullContext(c), new(BootstrapCommand)) c.Assert(<-errc, gc.IsNil) c.Assert((<-opc).(dummy.OpPutFile).Env, gc.Equals, "peckham") list, err := envtools.FindTools(env, version.Current.Major, version.Current.Minor, coretools.Filter{}, false) @@ -528,7 +530,7 @@ func (s *BootstrapSuite) TestAutoUploadOnlyForDev(c *gc.C) { s.setupAutoUploadTest(c, "1.8.3", "precise") - _, errc := runCommand(nullContext(), new(BootstrapCommand)) + _, errc := runCommand(nullContext(c), new(BootstrapCommand)) err := <-errc stripped := strings.Replace(err.Error(), "\n", "", -1) c.Assert(stripped, gc.Matches, noToolsAvailableMessage) @@ -540,8 +542,8 @@ code := cmd.Main(&BootstrapCommand{}, context, nil) c.Assert(code, gc.Equals, 1) errText := context.Stderr.(*bytes.Buffer).String() - errText = strings.Replace(errText, "\n", "", -1) - expectedErrText := "error: cannot upload bootstrap tools: Juju cannot bootstrap because no tools are available for your environment.*" + expectedErrText := "Bootstrap failed, destroying environment\n" + expectedErrText += "error: cannot upload bootstrap tools: Juju cannot bootstrap because no tools are available for your environment(.|\n)*" c.Assert(errText, gc.Matches, expectedErrText) } @@ -556,15 +558,16 @@ code := cmd.Main(&BootstrapCommand{}, context, nil) c.Assert(code, gc.Equals, 1) errText := context.Stderr.(*bytes.Buffer).String() - errText = strings.Replace(errText, "\n", "", -1) - expectedErrText := "error: cannot upload bootstrap tools: an error" + expectedErrText := "uploading tools for series \\[precise raring\\]\n" + expectedErrText += "Bootstrap failed, destroying environment\n" + expectedErrText += "error: cannot upload bootstrap tools: an error\n" c.Assert(errText, gc.Matches, expectedErrText) } func (s *BootstrapSuite) TestBootstrapDestroy(c *gc.C) { _, fake := makeEmptyFakeHome(c) defer fake.Restore() - opc, errc := runCommand(nullContext(), new(BootstrapCommand), "-e", "brokenenv") + opc, errc := runCommand(nullContext(c), new(BootstrapCommand), "-e", "brokenenv") err := <-errc c.Assert(err, gc.ErrorMatches, "dummy.Bootstrap is broken") var opDestroy *dummy.OpDestroy @@ -601,7 +604,7 @@ dummy.Reset() store, err := configstore.Default() c.Assert(err, gc.IsNil) - env, err := environs.PrepareFromName("peckham", nullContext(), store) + env, err := environs.PrepareFromName("peckham", nullContext(c), store) c.Assert(err, gc.IsNil) envtesting.RemoveAllTools(c, env) return env, fake diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/cmd_test.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/cmd_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/cmd_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/cmd_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -122,8 +122,9 @@ } } -func nullContext() *cmd.Context { - ctx := cmd.DefaultContext() +func nullContext(c *gc.C) *cmd.Context { + ctx, err := cmd.DefaultContext() + c.Assert(err, gc.IsNil) ctx.Stdin = io.LimitReader(nil, 0) ctx.Stdout = ioutil.Discard ctx.Stderr = ioutil.Discard diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/common.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/common.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/common.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/common.go 2014-03-27 15:48:42.000000000 +0000 @@ -11,10 +11,11 @@ ) // destroyPreparedEnviron destroys the environment and logs an error if it fails. -func destroyPreparedEnviron(env environs.Environ, store configstore.Storage, err *error, action string) { +func destroyPreparedEnviron(ctx *cmd.Context, env environs.Environ, store configstore.Storage, err *error, action string) { if *err == nil { return } + ctx.Infof("%s failed, destroying environment", action) if err := environs.Destroy(env, store); err != nil { logger.Errorf("%s failed, and the environment could not be destroyed: %v", action, err) } @@ -39,7 +40,7 @@ } cleanup := func() { if !existing { - destroyPreparedEnviron(environ, store, resultErr, action) + destroyPreparedEnviron(ctx, environ, store, resultErr, action) } } return environ, cleanup, nil diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/deploy.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/deploy.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/deploy.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/deploy.go 2014-03-27 15:48:42.000000000 +0000 @@ -150,7 +150,7 @@ } if c.BumpRevision { - ctx.Stdout.Write([]byte("--upgrade (or -u) is deprecated and ignored; charms are always deployed with a unique revision.\n")) + ctx.Infof("--upgrade (or -u) is deprecated and ignored; charms are always deployed with a unique revision.") } charmInfo, err := client.CharmInfo(curl.String()) @@ -293,7 +293,6 @@ default: return nil, fmt.Errorf("unsupported charm URL schema: %q", curl.Schema) } - report := fmt.Sprintf("Added charm %q to the environment.\n", curl) - ctx.Stdout.Write([]byte(report)) + ctx.Infof("Added charm %q to the environment.", curl) return curl, nil } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/deploy_test.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/deploy_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/deploy_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/deploy_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -83,8 +83,8 @@ ctx, err := coretesting.RunCommand(c, &DeployCommand{}, []string{"local:dummy", "-u"}) c.Assert(err, gc.IsNil) - c.Assert(coretesting.Stderr(ctx), gc.Equals, "") - output := strings.Split(coretesting.Stdout(ctx), "\n") + c.Assert(coretesting.Stdout(ctx), gc.Equals, "") + output := strings.Split(coretesting.Stderr(ctx), "\n") c.Check(output[0], gc.Matches, `Added charm ".*" to the environment.`) c.Check(output[1], gc.Equals, "--upgrade (or -u) is deprecated and ignored; charms are always deployed with a unique revision.") } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/destroyenvironment.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/destroyenvironment.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/destroyenvironment.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/destroyenvironment.go 2014-03-27 15:48:42.000000000 +0000 @@ -54,6 +54,11 @@ } environ, err := environs.NewFromName(c.envName, store) if err != nil { + if environs.IsEmptyConfig(err) { + // Delete the .jenv file and call it done. + ctx.Infof("removing empty environment file") + return environs.DestroyInfo(c.envName, store) + } return err } if !c.assumeYes { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/destroyenvironment_test.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/destroyenvironment_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/destroyenvironment_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/destroyenvironment_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -27,15 +27,15 @@ func (s *destroyEnvSuite) TestDestroyEnvironmentCommand(c *gc.C) { // Prepare the environment so we can destroy it. - _, err := environs.PrepareFromName("dummyenv", nullContext(), s.ConfigStore) + _, err := environs.PrepareFromName("dummyenv", nullContext(c), s.ConfigStore) c.Assert(err, gc.IsNil) // check environment is mandatory - opc, errc := runCommand(nullContext(), new(DestroyEnvironmentCommand)) + opc, errc := runCommand(nullContext(c), new(DestroyEnvironmentCommand)) c.Check(<-errc, gc.Equals, NoEnvironmentError) // normal destroy - opc, errc = runCommand(nullContext(), new(DestroyEnvironmentCommand), "dummyenv", "--yes") + opc, errc = runCommand(nullContext(c), new(DestroyEnvironmentCommand), "dummyenv", "--yes") c.Check(<-errc, gc.IsNil) c.Check((<-opc).(dummy.OpDestroy).Env, gc.Equals, "dummyenv") @@ -46,22 +46,22 @@ func (s *destroyEnvSuite) TestDestroyEnvironmentCommandEFlag(c *gc.C) { // Prepare the environment so we can destroy it. - _, err := environs.PrepareFromName("dummyenv", nullContext(), s.ConfigStore) + _, err := environs.PrepareFromName("dummyenv", nullContext(c), s.ConfigStore) c.Assert(err, gc.IsNil) // check that either environment or the flag is mandatory - opc, errc := runCommand(nullContext(), new(DestroyEnvironmentCommand)) + opc, errc := runCommand(nullContext(c), new(DestroyEnvironmentCommand)) c.Check(<-errc, gc.Equals, NoEnvironmentError) // We don't allow them to supply both entries at the same time - opc, errc = runCommand(nullContext(), new(DestroyEnvironmentCommand), "-e", "dummyenv", "dummyenv", "--yes") + opc, errc = runCommand(nullContext(c), new(DestroyEnvironmentCommand), "-e", "dummyenv", "dummyenv", "--yes") c.Check(<-errc, gc.Equals, DoubleEnvironmentError) // We treat --environment the same way - opc, errc = runCommand(nullContext(), new(DestroyEnvironmentCommand), "--environment", "dummyenv", "dummyenv", "--yes") + opc, errc = runCommand(nullContext(c), new(DestroyEnvironmentCommand), "--environment", "dummyenv", "dummyenv", "--yes") c.Check(<-errc, gc.Equals, DoubleEnvironmentError) // destroy using the -e flag - opc, errc = runCommand(nullContext(), new(DestroyEnvironmentCommand), "-e", "dummyenv", "--yes") + opc, errc = runCommand(nullContext(c), new(DestroyEnvironmentCommand), "-e", "dummyenv", "--yes") c.Check(<-errc, gc.IsNil) c.Check((<-opc).(dummy.OpDestroy).Env, gc.Equals, "dummyenv") @@ -70,6 +70,16 @@ c.Assert(err, jc.Satisfies, errors.IsNotFoundError) } +func (s *destroyEnvSuite) TestDestroyEnvironmentCommandEmptyJenv(c *gc.C) { + _, err := s.ConfigStore.CreateInfo("emptyenv") + c.Assert(err, gc.IsNil) + + context, err := coretesting.RunCommand(c, new(DestroyEnvironmentCommand), []string{"-e", "emptyenv"}) + c.Assert(err, gc.IsNil) + + c.Assert(coretesting.Stderr(context), gc.Equals, "removing empty environment file\n") +} + func (s *destroyEnvSuite) TestDestroyEnvironmentCommandBroken(c *gc.C) { oldinfo, err := s.ConfigStore.ReadInfo("dummyenv") c.Assert(err, gc.IsNil) @@ -89,11 +99,11 @@ c.Assert(err, gc.IsNil) // Prepare the environment so we can destroy it. - _, err = environs.PrepareFromName("dummyenv", nullContext(), s.ConfigStore) + _, err = environs.PrepareFromName("dummyenv", nullContext(c), s.ConfigStore) c.Assert(err, gc.IsNil) // destroy with broken environment - opc, errc := runCommand(nullContext(), new(DestroyEnvironmentCommand), "dummyenv", "--yes") + opc, errc := runCommand(nullContext(c), new(DestroyEnvironmentCommand), "dummyenv", "--yes") op, ok := (<-opc).(dummy.OpDestroy) c.Assert(ok, jc.IsTrue) c.Assert(op.Error, gc.ErrorMatches, "dummy.Destroy is broken") @@ -117,12 +127,13 @@ func (s *destroyEnvSuite) TestDestroyEnvironmentCommandConfirmation(c *gc.C) { var stdin, stdout bytes.Buffer - ctx := cmd.DefaultContext() + ctx, err := cmd.DefaultContext() + c.Assert(err, gc.IsNil) ctx.Stdout = &stdout ctx.Stdin = &stdin // Prepare the environment so we can destroy it. - env, err := environs.PrepareFromName("dummyenv", nullContext(), s.ConfigStore) + env, err := environs.PrepareFromName("dummyenv", nullContext(c), s.ConfigStore) c.Assert(err, gc.IsNil) assertEnvironNotDestroyed(c, env, s.ConfigStore) @@ -156,7 +167,7 @@ for _, answer := range []string{"y", "Y", "yes", "YES"} { // Prepare the environment so we can destroy it. s.Reset(c) - env, err := environs.PrepareFromName("dummyenv", nullContext(), s.ConfigStore) + env, err := environs.PrepareFromName("dummyenv", nullContext(c), s.ConfigStore) c.Assert(err, gc.IsNil) stdin.Reset() diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/environment.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/environment.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/environment.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/environment.go 2014-03-27 15:48:42.000000000 +0000 @@ -181,3 +181,65 @@ } return err } + +// UnsetEnvironment +type UnsetEnvironmentCommand struct { + cmd.EnvCommandBase + keys []string +} + +const unsetEnvHelpDoc = ` +Reset one or more the environment configuration attributes to its default +value in a running Juju instance. Attributes without defaults are removed, +and attempting to remove a required attribute with no default will result +in an error. + +Multiple attributes may be removed at once; keys are space-separated. +` + +func (c *UnsetEnvironmentCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "unset-environment", + Args: " ...", + Purpose: "unset environment values", + Doc: strings.TrimSpace(unsetEnvHelpDoc), + Aliases: []string{"unset-env"}, + } +} + +func (c *UnsetEnvironmentCommand) Init(args []string) (err error) { + if len(args) == 0 { + return fmt.Errorf("No keys specified") + } + c.keys = args + return nil +} + +// run1dot16 runs matches client.EnvironmentUnset using a direct DB +// connection to maintain compatibility with an API server running 1.16 or +// older (when EnvironmentUnset was not available). This fallback can be removed +// when we no longer maintain 1.16 compatibility. +func (c *UnsetEnvironmentCommand) run1dot16() error { + conn, err := juju.NewConnFromName(c.EnvName) + if err != nil { + return err + } + defer conn.Close() + return conn.State.UpdateEnvironConfig(nil, c.keys, nil) +} + +func (c *UnsetEnvironmentCommand) Run(ctx *cmd.Context) error { + client, err := juju.NewAPIClientFromName(c.EnvName) + if err != nil { + return err + } + defer client.Close() + + err = client.EnvironmentUnset(c.keys...) + if params.IsCodeNotImplemented(err) { + logger.Infof("EnvironmentUnset not supported by the API server, " + + "falling back to 1.16 compatibility mode (direct DB access)") + err = c.run1dot16() + } + return err +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/environment_test.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/environment_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/environment_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/environment_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -7,8 +7,10 @@ "fmt" "strings" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" + "launchpad.net/juju-core/environs/config" jujutesting "launchpad.net/juju-core/juju/testing" "launchpad.net/juju-core/provider/dummy" _ "launchpad.net/juju-core/provider/local" @@ -178,3 +180,73 @@ c.Assert(err, gc.ErrorMatches, errorPattern) } } + +type UnsetEnvironmentSuite struct { + jujutesting.RepoSuite +} + +var _ = gc.Suite(&UnsetEnvironmentSuite{}) + +var unsetEnvTests = []struct { + args []string + err string + expected attributes + unexpected []string +}{ + { + args: []string{}, + err: "No keys specified", + }, { + args: []string{"xyz", "xyz"}, + unexpected: []string{"xyz"}, + }, { + args: []string{"type", "xyz"}, + err: "type: expected string, got nothing", + expected: attributes{ + "type": "dummy", + "xyz": 123, + }, + }, { + args: []string{"syslog-port"}, + expected: attributes{ + "syslog-port": config.DefaultSyslogPort, + }, + }, { + args: []string{"xyz2", "xyz"}, + unexpected: []string{"xyz"}, + }, +} + +func (s *UnsetEnvironmentSuite) initConfig(c *gc.C) { + err := s.State.UpdateEnvironConfig(map[string]interface{}{ + "syslog-port": 1234, + "xyz": 123, + }, nil, nil) + c.Assert(err, gc.IsNil) +} + +func (s *UnsetEnvironmentSuite) TestUnsetEnvironment(c *gc.C) { + for _, t := range unsetEnvTests { + c.Logf("testing unset-env %v", t.args) + s.initConfig(c) + _, err := testing.RunCommand(c, &UnsetEnvironmentCommand{}, t.args) + if t.err != "" { + c.Assert(err, gc.ErrorMatches, t.err) + } else { + c.Assert(err, gc.IsNil) + } + if len(t.expected)+len(t.unexpected) != 0 { + stateConfig, err := s.State.EnvironConfig() + c.Assert(err, gc.IsNil) + for k, v := range t.expected { + vstate, ok := stateConfig.AllAttrs()[k] + c.Assert(ok, jc.IsTrue) + c.Assert(vstate, gc.Equals, v) + } + for _, k := range t.unexpected { + _, ok := stateConfig.AllAttrs()[k] + c.Assert(ok, jc.IsFalse) + } + } + } +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/main.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/main.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/main.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/main.go 2014-03-27 15:48:42.000000000 +0000 @@ -33,7 +33,12 @@ // 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) { - if err := juju.InitJujuHome(); err != nil { + ctx, err := cmd.DefaultContext() + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(2) + } + if err = juju.InitJujuHome(); err != nil { fmt.Fprintf(os.Stderr, "error: %s\n", err) os.Exit(2) } @@ -93,6 +98,7 @@ jujucmd.Register(wrap(&ResolvedCommand{})) jujucmd.Register(wrap(&DebugLogCommand{sshCmd: &SSHCommand{}})) jujucmd.Register(wrap(&DebugHooksCommand{})) + jujucmd.Register(wrap(&RetryProvisioningCommand{})) // Configuration commands. jujucmd.Register(wrap(&InitCommand{})) @@ -103,6 +109,7 @@ jujucmd.Register(wrap(&SetConstraintsCommand{})) jujucmd.Register(wrap(&GetEnvironmentCommand{})) jujucmd.Register(wrap(&SetEnvironmentCommand{})) + jujucmd.Register(wrap(&UnsetEnvironmentCommand{})) jujucmd.Register(wrap(&ExposeCommand{})) jujucmd.Register(wrap(&SyncToolsCommand{})) jujucmd.Register(wrap(&UnexposeCommand{})) @@ -121,7 +128,7 @@ // Common commands. jujucmd.Register(wrap(&cmd.VersionCommand{})) - os.Exit(cmd.Main(jujucmd, cmd.DefaultContext(), args[1:])) + os.Exit(cmd.Main(jujucmd, ctx, args[1:])) } // wrap encapsulates code that wraps some of the commands in a helper class diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/main_test.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/main_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/main_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/main_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -239,6 +239,7 @@ "remove-service", // alias for destroy-service "remove-unit", // alias for destroy-unit "resolved", + "retry-provisioning", "run", "scp", "set", @@ -253,6 +254,8 @@ "terminate-machine", // alias for destroy-machine "unexpose", "unset", + "unset-env", // alias for unset-environment + "unset-environment", "upgrade-charm", "upgrade-juju", "version", @@ -316,6 +319,7 @@ "-h, --help .*", "--log-file .*", "--logging-config .*", + "-q, --quiet .*", "--show-log .*", "-v, --verbose .*", } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/retryprovisioning.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/retryprovisioning.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/retryprovisioning.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/retryprovisioning.go 2014-03-27 15:48:42.000000000 +0000 @@ -0,0 +1,59 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package main + +import ( + "fmt" + + "launchpad.net/juju-core/cmd" + "launchpad.net/juju-core/juju" + "launchpad.net/juju-core/names" +) + +// RetryProvisioningCommand updates machines' error status to tell +// the provisoner that it should try to re-provision the machine. +type RetryProvisioningCommand struct { + cmd.EnvCommandBase + Machines []string +} + +func (c *RetryProvisioningCommand) Info() *cmd.Info { + return &cmd.Info{ + Name: "retry-provisioning", + Args: " [...]", + Purpose: "retries provisioning for failed machines", + } +} + +func (c *RetryProvisioningCommand) Init(args []string) error { + if len(args) == 0 { + return fmt.Errorf("no machine specified") + } + c.Machines = make([]string, len(args)) + for i, arg := range args { + if !names.IsMachine(arg) { + return fmt.Errorf("invalid machine %q", arg) + } + c.Machines[i] = names.MachineTag(arg) + } + return nil +} + +func (c *RetryProvisioningCommand) Run(context *cmd.Context) error { + client, err := juju.NewAPIClientFromName(c.EnvName) + if err != nil { + return err + } + defer client.Close() + results, err := client.RetryProvisioning(c.Machines...) + if err != nil { + return err + } + for i, result := range results { + if result.Error != nil { + fmt.Fprintf(context.Stderr, "cannot retry provisioning %q: %v\n", c.Machines[i], result.Error) + } + } + return nil +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/retryprovisioning_test.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/retryprovisioning_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/retryprovisioning_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/retryprovisioning_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -0,0 +1,82 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package main + +import ( + "strings" + + jc "github.com/juju/testing/checkers" + gc "launchpad.net/gocheck" + + jujutesting "launchpad.net/juju-core/juju/testing" + "launchpad.net/juju-core/state" + "launchpad.net/juju-core/state/api/params" + "launchpad.net/juju-core/testing" +) + +type retryProvisioningSuite struct { + jujutesting.JujuConnSuite +} + +var _ = gc.Suite(&retryProvisioningSuite{}) + +var resolvedMachineTests = []struct { + args []string + err string + stdErr string +}{ + { + err: `no machine specified`, + }, { + args: []string{"jeremy-fisher"}, + err: `invalid machine "jeremy-fisher"`, + }, { + args: []string{"42"}, + stdErr: `cannot retry provisioning "machine-42": machine 42 not found`, + }, { + args: []string{"1"}, + stdErr: `cannot retry provisioning "machine-1": machine "machine-1" is not in an error state`, + }, { + args: []string{"0"}, + }, { + args: []string{"0", "1"}, + stdErr: `cannot retry provisioning "machine-1": machine "machine-1" is not in an error state`, + }, +} + +func (s *retryProvisioningSuite) TestResolved(c *gc.C) { + m, err := s.State.AddOneMachine(state.MachineTemplate{ + Series: "quantal", + Jobs: []state.MachineJob{state.JobManageEnviron}, + }) + c.Assert(err, gc.IsNil) + err = m.SetStatus(params.StatusError, "broken", nil) + c.Assert(err, gc.IsNil) + _, err = s.State.AddOneMachine(state.MachineTemplate{ + Series: "quantal", + Jobs: []state.MachineJob{state.JobHostUnits}, + }) + c.Assert(err, gc.IsNil) + + for i, t := range resolvedMachineTests { + c.Logf("test %d: %v", i, t.args) + context, err := testing.RunCommand(c, &RetryProvisioningCommand{}, t.args) + if t.err != "" { + c.Check(err, gc.ErrorMatches, t.err) + continue + } else { + c.Check(err, gc.IsNil) + } + output := testing.Stderr(context) + stripped := strings.Replace(output, "\n", "", -1) + c.Check(stripped, gc.Equals, t.stdErr) + if t.args[0] == "0" { + status, info, data, err := m.Status() + c.Check(err, gc.IsNil) + c.Check(status, gc.Equals, params.StatusError) + c.Check(info, gc.Equals, "broken") + c.Check(data["transient"], jc.IsTrue) + } + } +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/status.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/status.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/status.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/status.go 2014-03-27 15:48:42.000000000 +0000 @@ -158,6 +158,7 @@ Exposed bool `json:"exposed" yaml:"exposed"` Life string `json:"life,omitempty" yaml:"life,omitempty"` Relations map[string][]string `json:"relations,omitempty" yaml:"relations,omitempty"` + Networks map[string][]string `json:"networks,omitempty" yaml:"networks,omitempty"` SubordinateTo []string `json:"subordinate-to,omitempty" yaml:"subordinate-to,omitempty"` Units map[string]unitStatus `json:"units,omitempty" yaml:"units,omitempty"` } @@ -256,10 +257,17 @@ Exposed: service.Exposed, Life: service.Life, Relations: service.Relations, + Networks: make(map[string][]string), CanUpgradeTo: service.CanUpgradeTo, SubordinateTo: service.SubordinateTo, Units: make(map[string]unitStatus), } + if len(service.Networks.Enabled) > 0 { + out.Networks["enabled"] = service.Networks.Enabled + } + if len(service.Networks.Disabled) > 0 { + out.Networks["disabled"] = service.Networks.Disabled + } for k, m := range service.Units { out.Units[k] = formatUnit(m) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/status_test.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/status_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/status_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/status_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -11,6 +11,7 @@ "strings" "time" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/goyaml" @@ -299,8 +300,56 @@ }, }, ), test( + "deploy two services with networks", + addMachine{machineId: "0", job: state.JobManageEnviron}, + startAliveMachine{"0"}, + setMachineStatus{"0", params.StatusStarted, ""}, + setAddresses{"0", []instance.Address{ + instance.NewAddress("10.0.0.1"), + instance.NewAddress("dummyenv-0.dns"), + }}, + addCharm{"dummy"}, + addService{ + name: "networks-service", + charm: "dummy", + withNetworks: []string{"net1", "net2"}, + withoutNetworks: []string{"net3", "net4"}, + }, + addService{ + name: "no-networks-service", + charm: "dummy", + withoutNetworks: []string{"mynet"}, + }, + + expect{ + "simulate just the two services and a bootstrap node", + M{ + "environment": "dummyenv", + "machines": M{ + "0": machine0, + }, + "services": M{ + "networks-service": M{ + "charm": "cs:quantal/dummy-1", + "exposed": false, + "networks": M{ + "enabled": L{"net1", "net2"}, + "disabled": L{"net3", "net4"}, + }, + }, + "no-networks-service": M{ + "charm": "cs:quantal/dummy-1", + "exposed": false, + "networks": M{ + "disabled": L{"mynet"}, + }, + }, + }, + }, + }, + ), test( "instance with different hardware characteristics", - addMachine{"0", machineCons, state.JobManageEnviron}, + addMachine{machineId: "0", cons: machineCons, job: state.JobManageEnviron}, setAddresses{"0", []instance.Address{ instance.NewAddress("10.0.0.1"), instance.NewAddress("dummyenv-0.dns"), @@ -325,7 +374,7 @@ }, ), test( "instance without addresses", - addMachine{"0", machineCons, state.JobManageEnviron}, + addMachine{machineId: "0", cons: machineCons, job: state.JobManageEnviron}, startAliveMachine{"0"}, setMachineStatus{"0", params.StatusStarted, ""}, expect{ @@ -384,8 +433,8 @@ startAliveMachine{"0"}, setMachineStatus{"0", params.StatusStarted, ""}, addCharm{"dummy"}, - addService{"dummy-service", "dummy"}, - addService{"exposed-service", "dummy"}, + addService{name: "dummy-service", charm: "dummy"}, + addService{name: "exposed-service", charm: "dummy"}, expect{ "no services exposed yet", M{ @@ -711,11 +760,10 @@ }, }, }, - ), - test( + ), test( "add a dying service", addCharm{"dummy"}, - addService{"dummy-service", "dummy"}, + addService{name: "dummy-service", charm: "dummy"}, addMachine{machineId: "0", job: state.JobHostUnits}, addUnit{"dummy-service", "0"}, ensureDyingService{"dummy-service"}, @@ -757,7 +805,7 @@ addCharm{"mysql"}, addCharm{"varnish"}, - addService{"project", "wordpress"}, + addService{name: "project", charm: "wordpress"}, setServiceExposed{"project", true}, addMachine{machineId: "1", job: state.JobHostUnits}, setAddresses{"1", []instance.Address{instance.NewAddress("dummyenv-1.dns")}}, @@ -766,7 +814,7 @@ addAliveUnit{"project", "1"}, setUnitStatus{"project/0", params.StatusStarted, ""}, - addService{"mysql", "mysql"}, + addService{name: "mysql", charm: "mysql"}, setServiceExposed{"mysql", true}, addMachine{machineId: "2", job: state.JobHostUnits}, setAddresses{"2", []instance.Address{instance.NewAddress("dummyenv-2.dns")}}, @@ -775,7 +823,7 @@ addAliveUnit{"mysql", "2"}, setUnitStatus{"mysql/0", params.StatusStarted, ""}, - addService{"varnish", "varnish"}, + addService{name: "varnish", charm: "varnish"}, setServiceExposed{"varnish", true}, addMachine{machineId: "3", job: state.JobHostUnits}, setAddresses{"3", []instance.Address{instance.NewAddress("dummyenv-3.dns")}}, @@ -783,7 +831,7 @@ setMachineStatus{"3", params.StatusStarted, ""}, addUnit{"varnish", "3"}, - addService{"private", "wordpress"}, + addService{name: "private", charm: "wordpress"}, setServiceExposed{"private", true}, addMachine{machineId: "4", job: state.JobHostUnits}, setAddresses{"4", []instance.Address{instance.NewAddress("dummyenv-4.dns")}}, @@ -876,7 +924,7 @@ addCharm{"riak"}, addCharm{"wordpress"}, - addService{"riak", "riak"}, + addService{name: "riak", charm: "riak"}, setServiceExposed{"riak", true}, addMachine{machineId: "1", job: state.JobHostUnits}, setAddresses{"1", []instance.Address{instance.NewAddress("dummyenv-1.dns")}}, @@ -948,7 +996,7 @@ addCharm{"mysql"}, addCharm{"logging"}, - addService{"wordpress", "wordpress"}, + addService{name: "wordpress", charm: "wordpress"}, setServiceExposed{"wordpress", true}, addMachine{machineId: "1", job: state.JobHostUnits}, setAddresses{"1", []instance.Address{instance.NewAddress("dummyenv-1.dns")}}, @@ -957,7 +1005,7 @@ addAliveUnit{"wordpress", "1"}, setUnitStatus{"wordpress/0", params.StatusStarted, ""}, - addService{"mysql", "mysql"}, + addService{name: "mysql", charm: "mysql"}, setServiceExposed{"mysql", true}, addMachine{machineId: "2", job: state.JobHostUnits}, setAddresses{"2", []instance.Address{instance.NewAddress("dummyenv-2.dns")}}, @@ -966,7 +1014,7 @@ addAliveUnit{"mysql", "2"}, setUnitStatus{"mysql/0", params.StatusStarted, ""}, - addService{"logging", "logging"}, + addService{name: "logging", charm: "logging"}, setServiceExposed{"logging", true}, relateServices{"wordpress", "mysql"}, @@ -1160,7 +1208,7 @@ addCharm{"logging"}, addCharm{"monitoring"}, - addService{"wordpress", "wordpress"}, + addService{name: "wordpress", charm: "wordpress"}, setServiceExposed{"wordpress", true}, addMachine{machineId: "1", job: state.JobHostUnits}, setAddresses{"1", []instance.Address{instance.NewAddress("dummyenv-1.dns")}}, @@ -1169,9 +1217,9 @@ addAliveUnit{"wordpress", "1"}, setUnitStatus{"wordpress/0", params.StatusStarted, ""}, - addService{"logging", "logging"}, + addService{name: "logging", charm: "logging"}, setServiceExposed{"logging", true}, - addService{"monitoring", "monitoring"}, + addService{name: "monitoring", charm: "monitoring"}, setServiceExposed{"monitoring", true}, relateServices{"wordpress", "logging"}, @@ -1234,7 +1282,7 @@ startAliveMachine{"0"}, setMachineStatus{"0", params.StatusStarted, ""}, addCharm{"mysql"}, - addService{"mysql", "mysql"}, + addService{name: "mysql", charm: "mysql"}, setServiceExposed{"mysql", true}, addMachine{machineId: "1", job: state.JobHostUnits}, @@ -1323,7 +1371,7 @@ startAliveMachine{"1"}, setMachineStatus{"1", params.StatusStarted, ""}, addCharm{"mysql"}, - addService{"mysql", "mysql"}, + addService{name: "mysql", charm: "mysql"}, setServiceExposed{"mysql", true}, addCharmPlaceholder{"mysql", 23}, addAliveUnit{"mysql", "1"}, @@ -1363,7 +1411,7 @@ startAliveMachine{"1"}, setMachineStatus{"1", params.StatusStarted, ""}, addCharm{"mysql"}, - addService{"mysql", "mysql"}, + addService{name: "mysql", charm: "mysql"}, setServiceExposed{"mysql", true}, addAliveUnit{"mysql", "1"}, setUnitCharmURL{"mysql/0", "cs:quantal/mysql-1"}, @@ -1405,7 +1453,7 @@ startAliveMachine{"1"}, setMachineStatus{"1", params.StatusStarted, ""}, addCharm{"mysql"}, - addService{"mysql", "mysql"}, + addService{name: "mysql", charm: "mysql"}, setServiceExposed{"mysql", true}, addAliveUnit{"mysql", "1"}, setUnitCharmURL{"mysql/0", "cs:quantal/mysql-1"}, @@ -1449,7 +1497,7 @@ startAliveMachine{"1"}, setMachineStatus{"1", params.StatusStarted, ""}, addCharm{"mysql"}, - addService{"mysql", "mysql"}, + addService{name: "mysql", charm: "mysql"}, setServiceExposed{"mysql", true}, addAliveUnit{"mysql", "1"}, setUnitCharmURL{"mysql/0", "cs:quantal/mysql-1"}, @@ -1619,14 +1667,16 @@ } type addService struct { - name string - charm string + name string + charm string + withNetworks []string + withoutNetworks []string } func (as addService) step(c *gc.C, ctx *context) { ch, ok := ctx.charms[as.charm] c.Assert(ok, gc.Equals, true) - _, err := ctx.st.AddService(as.name, "user-admin", ch) + _, err := ctx.st.AddService(as.name, "user-admin", ch, as.withNetworks, as.withoutNetworks) c.Assert(err, gc.IsNil) } @@ -1876,7 +1926,7 @@ actual := make(M) err = format.unmarshal(stdout, &actual) c.Assert(err, gc.IsNil) - c.Assert(actual, gc.DeepEquals, expected) + c.Assert(actual, jc.DeepEquals, expected) } } @@ -1901,7 +1951,7 @@ addMachine{machineId: "0", job: state.JobManageEnviron}, addMachine{machineId: "1", job: state.JobHostUnits}, addCharm{"mysql"}, - addService{"mysql", "mysql"}, + addService{name: "mysql", charm: "mysql"}, addAliveUnit{"mysql", "1"}, } ctx := s.newContext() diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/synctools_test.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/synctools_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/juju/synctools_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/juju/synctools_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -125,7 +125,7 @@ func (s *syncToolsSuite) TestSyncToolsCommand(c *gc.C) { for i, test := range syncToolsCommandTests { c.Logf("test %d: %s", i, test.description) - targetEnv, err := environs.PrepareFromName("test-target", nullContext(), s.configStore) + targetEnv, err := environs.PrepareFromName("test-target", nullContext(c), s.configStore) c.Assert(err, gc.IsNil) called := false syncTools = func(sctx *sync.SyncContext) error { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/bootstrap.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/jujud/bootstrap.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/bootstrap.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/jujud/bootstrap.go 2014-03-27 15:48:42.000000000 +0000 @@ -6,8 +6,6 @@ import ( "encoding/base64" "fmt" - "io/ioutil" - "strings" "launchpad.net/gnuflag" "launchpad.net/goyaml" @@ -16,23 +14,19 @@ "launchpad.net/juju-core/cmd" "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/environs" - "launchpad.net/juju-core/environs/bootstrap" - "launchpad.net/juju-core/environs/cloudinit" "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/instance" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api/params" ) -// Cloud-init write the URL to be used to load the bootstrap state into this file. -// A variable is used here to allow tests to override. -var providerStateURLFile = cloudinit.BootstrapStateURLFile - type BootstrapCommand struct { cmd.CommandBase Conf AgentConf EnvConfig map[string]interface{} Constraints constraints.Value + Hardware instance.HardwareCharacteristics + InstanceId string } // Info returns a decription of the command. @@ -47,6 +41,8 @@ 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)") + f.Var(&c.Hardware, "hardware", "hardware characteristics (space-separated strings)") + f.StringVar(&c.InstanceId, "instance-id", "", "unique instance-id for bootstrap machine") } // Init initializes the command for running. @@ -54,26 +50,19 @@ if len(c.EnvConfig) == 0 { return requiredError("env-config") } + if c.InstanceId == "" { + return requiredError("instance-id") + } return c.Conf.checkArgs(args) } // Run initializes state for an environment. func (c *BootstrapCommand) Run(_ *cmd.Context) error { - data, err := ioutil.ReadFile(providerStateURLFile) - if err != nil { - return fmt.Errorf("cannot read provider-state-url file: %v", err) - } envCfg, err := config.New(config.NoDefaults, c.EnvConfig) if err != nil { return err } - stateInfoURL := strings.Split(string(data), "\n")[0] - bsState, err := bootstrap.LoadStateFromURL(stateInfoURL, !envCfg.SSLHostnameVerification()) - if err != nil { - return fmt.Errorf("cannot load state from URL %q (read from %q): %v", stateInfoURL, providerStateURLFile, err) - } - err = c.Conf.read("machine-0") - if err != nil { + if err := c.Conf.read("machine-0"); err != nil { return err } // agent.Jobs is an optional field in the agent config, and was @@ -86,15 +75,11 @@ params.JobHostUnits, } } - var characteristics instance.HardwareCharacteristics - if len(bsState.Characteristics) > 0 { - characteristics = bsState.Characteristics[0] - } st, _, err := c.Conf.config.InitializeState(envCfg, agent.BootstrapMachineConfig{ Constraints: c.Constraints, Jobs: jobs, - InstanceId: bsState.StateInstances[0], - Characteristics: characteristics, + InstanceId: instance.Id(c.InstanceId), + Characteristics: c.Hardware, }, state.DefaultDialOpts(), environs.NewStatePolicy()) if err != nil { return err diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/bootstrap_test.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/jujud/bootstrap_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/bootstrap_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/jujud/bootstrap_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -5,8 +5,6 @@ import ( "encoding/base64" - "io/ioutil" - "path/filepath" jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" @@ -15,8 +13,6 @@ "launchpad.net/juju-core/agent" "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/environs" - "launchpad.net/juju-core/environs/bootstrap" - "launchpad.net/juju-core/environs/jujutest" "launchpad.net/juju-core/errors" "launchpad.net/juju-core/instance" "launchpad.net/juju-core/provider/dummy" @@ -33,32 +29,15 @@ type BootstrapSuite struct { testbase.LoggingSuite testing.MgoSuite - dataDir string - logDir string - providerStateURLFile string + dataDir string + logDir string } var _ = gc.Suite(&BootstrapSuite{}) -var testRoundTripper = &jujutest.ProxyRoundTripper{} - -func init() { - // Prepare mock http transport for provider-state output in tests. - testRoundTripper.RegisterForScheme("test") -} - func (s *BootstrapSuite) SetUpSuite(c *gc.C) { s.LoggingSuite.SetUpSuite(c) s.MgoSuite.SetUpSuite(c) - stateInfo := bootstrap.BootstrapState{ - StateInstances: []instance.Id{instance.Id("dummy.instance.id")}, - } - stateData, err := goyaml.Marshal(stateInfo) - c.Assert(err, gc.IsNil) - content := map[string]string{"/" + bootstrap.StateFile: string(stateData)} - testRoundTripper.Sub = jujutest.NewCannedRoundTripper(content, nil) - s.providerStateURLFile = filepath.Join(c.MkDir(), "provider-state-url") - providerStateURLFile = s.providerStateURLFile } func (s *BootstrapSuite) TearDownSuite(c *gc.C) { @@ -85,7 +64,6 @@ } func (s *BootstrapSuite) initBootstrapCommand(c *gc.C, jobs []params.MachineJob, args ...string) (machineConf agent.Config, cmd *BootstrapCommand, err error) { - ioutil.WriteFile(s.providerStateURLFile, []byte("test://localhost/provider-state\n"), 0600) if len(jobs) == 0 { // Add default jobs. jobs = []params.MachineJob{ @@ -123,7 +101,8 @@ } func (s *BootstrapSuite) TestInitializeEnvironment(c *gc.C) { - _, cmd, err := s.initBootstrapCommand(c, nil, "--env-config", testConfig) + hw := instance.MustParseHardware("arch=amd64 mem=8G") + _, cmd, err := s.initBootstrapCommand(c, nil, "--env-config", testConfig, "--instance-id", "anything", "--hardware", hw.String()) c.Assert(err, gc.IsNil) err = cmd.Run(nil) c.Assert(err, gc.IsNil) @@ -141,7 +120,12 @@ instid, err := machines[0].InstanceId() c.Assert(err, gc.IsNil) - c.Assert(instid, gc.Equals, instance.Id("dummy.instance.id")) + c.Assert(instid, gc.Equals, instance.Id("anything")) + + stateHw, err := machines[0].HardwareCharacteristics() + c.Assert(err, gc.IsNil) + c.Assert(stateHw, gc.NotNil) + c.Assert(*stateHw, gc.DeepEquals, hw) cons, err := st.EnvironConstraints() c.Assert(err, gc.IsNil) @@ -150,7 +134,11 @@ func (s *BootstrapSuite) TestSetConstraints(c *gc.C) { tcons := constraints.Value{Mem: uint64p(2048), CpuCores: uint64p(2)} - _, cmd, err := s.initBootstrapCommand(c, nil, "--env-config", testConfig, "--constraints", tcons.String()) + _, cmd, err := s.initBootstrapCommand(c, nil, + "--env-config", testConfig, + "--instance-id", "anything", + "--constraints", tcons.String(), + ) c.Assert(err, gc.IsNil) err = cmd.Run(nil) c.Assert(err, gc.IsNil) @@ -182,7 +170,7 @@ expectedJobs := []state.MachineJob{ state.JobManageEnviron, state.JobHostUnits, } - _, cmd, err := s.initBootstrapCommand(c, nil, "--env-config", testConfig) + _, cmd, err := s.initBootstrapCommand(c, nil, "--env-config", testConfig, "--instance-id", "anything") c.Assert(err, gc.IsNil) err = cmd.Run(nil) c.Assert(err, gc.IsNil) @@ -201,7 +189,7 @@ func (s *BootstrapSuite) TestConfiguredMachineJobs(c *gc.C) { jobs := []params.MachineJob{params.JobManageEnviron} - _, cmd, err := s.initBootstrapCommand(c, jobs, "--env-config", testConfig) + _, cmd, err := s.initBootstrapCommand(c, jobs, "--env-config", testConfig, "--instance-id", "anything") c.Assert(err, gc.IsNil) err = cmd.Run(nil) c.Assert(err, gc.IsNil) @@ -231,7 +219,7 @@ } func (s *BootstrapSuite) TestInitialPassword(c *gc.C) { - machineConf, cmd, err := s.initBootstrapCommand(c, nil, "--env-config", testConfig) + machineConf, cmd, err := s.initBootstrapCommand(c, nil, "--env-config", testConfig, "--instance-id", "anything") c.Assert(err, gc.IsNil) err = cmd.Run(nil) @@ -270,35 +258,65 @@ defer st.Close() } -var base64ConfigTests = []struct { - input []string - err string - expected map[string]interface{} +var bootstrapArgTests = []struct { + input []string + err string + expectedInstanceId string + expectedHardware instance.HardwareCharacteristics + expectedConfig map[string]interface{} }{ { - // no value supplied - nil, - "--env-config option must be set", - nil, + // no value supplied for env-config + err: "--env-config option must be set", }, { - // empty - []string{"--env-config", ""}, - "--env-config option must be set", - nil, + // empty env-config + input: []string{"--env-config", ""}, + err: "--env-config option must be set", }, { // wrong, should be base64 - []string{"--env-config", "name: banana\n"}, - ".*illegal base64 data at input byte.*", - nil, + input: []string{"--env-config", "name: banana\n"}, + err: ".*illegal base64 data at input byte.*", + }, { + // no value supplied for instance-id + input: []string{ + "--env-config", base64.StdEncoding.EncodeToString([]byte("name: banana\n")), + }, + err: "--instance-id option must be set", + }, { + // empty instance-id + input: []string{ + "--env-config", base64.StdEncoding.EncodeToString([]byte("name: banana\n")), + "--instance-id", "", + }, + err: "--instance-id option must be set", + }, { + input: []string{ + "--env-config", base64.StdEncoding.EncodeToString([]byte("name: banana\n")), + "--instance-id", "anything", + }, + expectedInstanceId: "anything", + expectedConfig: map[string]interface{}{"name": "banana"}, + }, { + input: []string{ + "--env-config", base64.StdEncoding.EncodeToString([]byte("name: banana\n")), + "--instance-id", "anything", + "--hardware", "nonsense", + }, + err: `invalid value "nonsense" for flag --hardware: malformed characteristic "nonsense"`, }, { - []string{"--env-config", base64.StdEncoding.EncodeToString([]byte("name: banana\n"))}, - "", - map[string]interface{}{"name": "banana"}, + input: []string{ + "--env-config", base64.StdEncoding.EncodeToString([]byte("name: banana\n")), + "--instance-id", "anything", + "--hardware", "arch=amd64 cpu-cores=4 root-disk=2T", + }, + expectedInstanceId: "anything", + expectedHardware: instance.MustParseHardware("arch=amd64 cpu-cores=4 root-disk=2T"), + expectedConfig: map[string]interface{}{"name": "banana"}, }, } -func (s *BootstrapSuite) TestBase64Config(c *gc.C) { - for i, t := range base64ConfigTests { +func (s *BootstrapSuite) TestBootstrapArgs(c *gc.C) { + for i, t := range bootstrapArgTests { c.Logf("test %d", i) var args []string args = append(args, t.input...) @@ -306,7 +324,9 @@ if t.err == "" { c.Assert(cmd, gc.NotNil) c.Assert(err, gc.IsNil) - c.Assert(cmd.EnvConfig, gc.DeepEquals, t.expected) + c.Assert(cmd.EnvConfig, gc.DeepEquals, t.expectedConfig) + c.Assert(cmd.InstanceId, gc.Equals, t.expectedInstanceId) + c.Assert(cmd.Hardware, gc.DeepEquals, t.expectedHardware) } else { c.Assert(err, gc.ErrorMatches, t.err) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/main.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/jujud/main.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/main.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/jujud/main.go 2014-03-27 15:48:42.000000000 +0000 @@ -94,7 +94,7 @@ // Main registers subcommands for the jujud executable, and hands over control // to the cmd package. -func jujuDMain(args []string) (code int, err error) { +func jujuDMain(args []string, ctx *cmd.Context) (code int, err error) { jujud := cmd.NewSuperCommand(cmd.SuperCommandParams{ Name: "jujud", Doc: jujudDoc, @@ -104,7 +104,7 @@ jujud.Register(&MachineAgent{}) jujud.Register(&UnitAgent{}) jujud.Register(&cmd.VersionCommand{}) - code = cmd.Main(jujud, cmd.DefaultContext(), args[1:]) + code = cmd.Main(jujud, ctx, args[1:]) return code, nil } @@ -112,16 +112,20 @@ // for testing with arbitrary command line arguments. func Main(args []string) { var code int = 1 - var err error + ctx, err := cmd.DefaultContext() + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(2) + } commandName := filepath.Base(args[0]) if commandName == "jujud" { - code, err = jujuDMain(args) + code, err = jujuDMain(args, ctx) } else if commandName == "jujuc" { fmt.Fprint(os.Stderr, jujudDoc) code = 2 err = fmt.Errorf("jujuc should not be called directly") } else if commandName == "juju-run" { - code = cmd.Main(&RunCommand{}, cmd.DefaultContext(), args[1:]) + code = cmd.Main(&RunCommand{}, ctx, args[1:]) } else { code, err = jujuCMain(commandName, args) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/main_test.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/jujud/main_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/jujud/main_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/jujud/main_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -110,6 +110,7 @@ checkMessage(c, msga, "bootstrap-state", "--env-config", b64yaml{"blah": "blah"}.encode(), + "--instance-id", "inst", "toastie") checkMessage(c, msga, "unit", "--unit-name", "un/0", diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/logging.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/logging.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/logging.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/logging.go 2014-03-27 15:48:42.000000000 +0000 @@ -26,6 +26,7 @@ type Log struct { Path string Verbose bool + Quiet bool Debug bool ShowLog bool Config string @@ -43,45 +44,51 @@ // AddFlags adds appropriate flags to f. func (l *Log) AddFlags(f *gnuflag.FlagSet) { f.StringVar(&l.Path, "log-file", "", "path to write log to") - // TODO(thumper): rename verbose to --show-log - 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") + f.BoolVar(&l.Verbose, "v", false, "show more verbose output") + f.BoolVar(&l.Verbose, "verbose", false, "show more verbose output") + f.BoolVar(&l.Quiet, "q", false, "show no informational output") + f.BoolVar(&l.Quiet, "quiet", false, "show no informational output") + f.BoolVar(&l.Debug, "debug", false, "equivalent to --show-log --log-config==DEBUG") defaultLogConfig := os.Getenv(osenv.JujuLoggingConfigEnvKey) f.StringVar(&l.Config, "logging-config", defaultLogConfig, "specify log levels for modules") f.BoolVar(&l.ShowLog, "show-log", false, "if set, write the log file to stderr") } // Start starts logging using the given Context. -func (l *Log) Start(ctx *Context) error { - if l.Path != "" { - path := ctx.AbsPath(l.Path) +func (log *Log) Start(ctx *Context) error { + if log.Verbose && log.Quiet { + return fmt.Errorf(`"verbose" and "quiet" flags clash, please use one or the other, not both`) + } + ctx.quiet = log.Quiet + ctx.verbose = log.Verbose + if log.Path != "" { + path := ctx.AbsPath(log.Path) target, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) if err != nil { return err } - writer := l.GetLogWriter(target) + writer := log.GetLogWriter(target) err = loggo.RegisterWriter("logfile", writer, loggo.TRACE) if err != nil { return err } } level := loggo.WARNING - if l.Verbose { - ctx.Stdout.Write([]byte("Flag --verbose is deprecated with the current meaning, use --show-log\n")) - l.ShowLog = true - } - if l.ShowLog { + if log.ShowLog { level = loggo.INFO } - if l.Debug { - l.ShowLog = true + if log.Debug { + log.ShowLog = true level = loggo.DEBUG + // override quiet or verbose if set, this way all the information goes + // to the log file. + ctx.quiet = true + ctx.verbose = false } - if l.ShowLog { + if log.ShowLog { // We replace the default writer to use ctx.Stderr rather than os.Stderr. - writer := l.GetLogWriter(ctx.Stderr) + writer := log.GetLogWriter(ctx.Stderr) _, err := loggo.ReplaceDefaultWriter(writer) if err != nil { return err @@ -99,7 +106,7 @@ // Set the level on the root logger. loggo.GetLogger("").SetLogLevel(level) // Override the logging config with specified logging config. - loggo.ConfigureLoggers(l.Config) + loggo.ConfigureLoggers(log.Config) return nil } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/logging_test.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/logging_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/logging_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/logging_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -25,6 +25,7 @@ var _ = gc.Suite(&LogSuite{}) func (s *LogSuite) SetUpTest(c *gc.C) { + s.CleanupSuite.SetUpTest(c) s.PatchEnvironment(osenv.JujuLoggingConfigEnvKey, "") s.AddCleanup(func(_ *gc.C) { loggo.ResetLoggers() @@ -44,6 +45,7 @@ func (s *LogSuite) TestNoFlags(c *gc.C) { log := newLogWithFlags(c, []string{}) c.Assert(log.Path, gc.Equals, "") + c.Assert(log.Quiet, gc.Equals, false) c.Assert(log.Verbose, gc.Equals, false) c.Assert(log.Debug, gc.Equals, false) c.Assert(log.Config, gc.Equals, "") @@ -68,17 +70,6 @@ c.Assert(log.Config, gc.Equals, config) } -func (s *LogSuite) TestVerboseSetsLogLevel(c *gc.C) { - l := &cmd.Log{Verbose: true} - ctx := coretesting.Context(c) - err := l.Start(ctx) - c.Assert(err, gc.IsNil) - - c.Assert(loggo.GetLogger("").LogLevel(), gc.Equals, loggo.INFO) - c.Assert(coretesting.Stderr(ctx), gc.Equals, "") - c.Assert(coretesting.Stdout(ctx), gc.Equals, "Flag --verbose is deprecated with the current meaning, use --show-log\n") -} - func (s *LogSuite) TestDebugSetsLogLevel(c *gc.C) { l := &cmd.Log{Debug: true} ctx := coretesting.Context(c) @@ -102,7 +93,7 @@ } func (s *LogSuite) TestStderr(c *gc.C) { - l := &cmd.Log{Verbose: true, Config: "=INFO"} + l := &cmd.Log{ShowLog: true, Config: "=INFO"} ctx := coretesting.Context(c) err := l.Start(ctx) c.Assert(err, gc.IsNil) @@ -161,3 +152,88 @@ c.Assert(coretesting.Stderr(ctx), gc.Matches, `^.*WARNING a warning\n.*ERROR an error\n.*`) c.Assert(coretesting.Stdout(ctx), gc.Equals, "") } + +func (s *LogSuite) TestQuietAndVerbose(c *gc.C) { + l := &cmd.Log{Verbose: true, Quiet: true} + ctx := coretesting.Context(c) + err := l.Start(ctx) + c.Assert(err, gc.ErrorMatches, `"verbose" and "quiet" flags clash, please use one or the other, not both`) +} + +func (s *LogSuite) TestOutputDefault(c *gc.C) { + l := &cmd.Log{} + ctx := coretesting.Context(c) + err := l.Start(ctx) + c.Assert(err, gc.IsNil) + + ctx.Infof("Writing info output") + ctx.Verbosef("Writing verbose output") + + c.Assert(coretesting.Stderr(ctx), gc.Equals, "Writing info output\n") +} + +func (s *LogSuite) TestOutputVerbose(c *gc.C) { + l := &cmd.Log{Verbose: true} + ctx := coretesting.Context(c) + err := l.Start(ctx) + c.Assert(err, gc.IsNil) + + ctx.Infof("Writing info output") + ctx.Verbosef("Writing verbose output") + + c.Assert(coretesting.Stderr(ctx), gc.Equals, "Writing info output\nWriting verbose output\n") +} + +func (s *LogSuite) TestOutputQuiet(c *gc.C) { + l := &cmd.Log{Quiet: true} + ctx := coretesting.Context(c) + err := l.Start(ctx) + c.Assert(err, gc.IsNil) + + ctx.Infof("Writing info output") + ctx.Verbosef("Writing verbose output") + + c.Assert(coretesting.Stderr(ctx), gc.Equals, "") +} + +func (s *LogSuite) TestOutputQuietLogs(c *gc.C) { + l := &cmd.Log{Quiet: true, Path: "foo.log", Config: "=INFO"} + ctx := coretesting.Context(c) + err := l.Start(ctx) + c.Assert(err, gc.IsNil) + + ctx.Infof("Writing info output") + ctx.Verbosef("Writing verbose output") + + content, err := ioutil.ReadFile(filepath.Join(ctx.Dir, "foo.log")) + c.Assert(err, gc.IsNil) + c.Assert(coretesting.Stderr(ctx), gc.Equals, "") + c.Assert(string(content), gc.Matches, `^.*INFO .* Writing info output\n.*INFO .*Writing verbose output\n.*`) +} + +func (s *LogSuite) TestOutputDefaultLogsVerbose(c *gc.C) { + l := &cmd.Log{Path: "foo.log", Config: "=INFO"} + ctx := coretesting.Context(c) + err := l.Start(ctx) + c.Assert(err, gc.IsNil) + + ctx.Infof("Writing info output") + ctx.Verbosef("Writing verbose output") + + content, err := ioutil.ReadFile(filepath.Join(ctx.Dir, "foo.log")) + c.Assert(err, gc.IsNil) + c.Assert(coretesting.Stderr(ctx), gc.Equals, "Writing info output\n") + c.Assert(string(content), gc.Matches, `^.*INFO .*Writing verbose output\n.*`) +} + +func (s *LogSuite) TestOutputDebugForcesQuiet(c *gc.C) { + l := &cmd.Log{Verbose: true, Debug: true} + ctx := coretesting.Context(c) + err := l.Start(ctx) + c.Assert(err, gc.IsNil) + + ctx.Infof("Writing info output") + ctx.Verbosef("Writing verbose output") + + c.Assert(coretesting.Stderr(ctx), gc.Matches, `^.*INFO .* Writing info output\n.*INFO .*Writing verbose output\n.*`) +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/metadata.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/metadata.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/metadata.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/metadata.go 2014-03-27 15:48:42.000000000 +0000 @@ -25,6 +25,11 @@ // 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) { + ctx, err := cmd.DefaultContext() + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(2) + } if err := juju.InitJujuHome(); err != nil { fmt.Fprintf(os.Stderr, "error: %s\n", err) os.Exit(2) @@ -42,7 +47,7 @@ metadatacmd.Register(&ValidateToolsMetadataCommand{}) metadatacmd.Register(&SignMetadataCommand{}) - os.Exit(cmd.Main(metadatacmd, cmd.DefaultContext(), args[1:])) + os.Exit(cmd.Main(metadatacmd, ctx, args[1:])) } func main() { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/toolsmetadata.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/toolsmetadata.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/toolsmetadata.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/toolsmetadata.go 2014-03-27 15:48:42.000000000 +0000 @@ -16,6 +16,7 @@ envtools "launchpad.net/juju-core/environs/tools" "launchpad.net/juju-core/juju/osenv" coretools "launchpad.net/juju-core/tools" + "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" ) @@ -62,7 +63,7 @@ if err != nil { return err } - sourceDataSource := simplestreams.NewURLDataSource("local source", source, simplestreams.VerifySSLHostnames) + sourceDataSource := simplestreams.NewURLDataSource("local source", source, utils.VerifySSLHostnames) toolsList, err = envtools.FindToolsForCloud( []simplestreams.DataSource{sourceDataSource}, simplestreams.CloudSpec{}, version.Current.Major, minorVersion, coretools.Filter{}) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validateimagemetadata.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validateimagemetadata.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validateimagemetadata.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validateimagemetadata.go 2014-03-27 15:48:42.000000000 +0000 @@ -17,6 +17,7 @@ "launchpad.net/juju-core/environs/configstore" "launchpad.net/juju-core/environs/imagemetadata" "launchpad.net/juju-core/environs/simplestreams" + "launchpad.net/juju-core/utils" ) // ValidateImageMetadataCommand @@ -184,7 +185,7 @@ } params.Sources = []simplestreams.DataSource{ simplestreams.NewURLDataSource( - "local metadata directory", "file://"+dir, simplestreams.VerifySSLHostnames), + "local metadata directory", "file://"+dir, utils.VerifySSLHostnames), } } params.Stream = c.stream diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validatetoolsmetadata.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validatetoolsmetadata.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validatetoolsmetadata.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/plugins/juju-metadata/validatetoolsmetadata.go 2014-03-27 15:48:42.000000000 +0000 @@ -16,6 +16,7 @@ "launchpad.net/juju-core/environs/simplestreams" "launchpad.net/juju-core/environs/tools" "launchpad.net/juju-core/juju/arch" + "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" ) @@ -199,7 +200,7 @@ return err } params.Sources = []simplestreams.DataSource{simplestreams.NewURLDataSource( - "local metadata directory", toolsURL, simplestreams.VerifySSLHostnames), + "local metadata directory", toolsURL, utils.VerifySSLHostnames), } } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-restore/restore.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/plugins/juju-restore/restore.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/juju-restore/restore.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/plugins/juju-restore/restore.go 2014-03-27 15:48:42.000000000 +0000 @@ -11,7 +11,6 @@ "io" "io/ioutil" "os" - "os/exec" "path" "text/template" @@ -31,6 +30,7 @@ "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" "launchpad.net/juju-core/utils" + "launchpad.net/juju-core/utils/ssh" ) func main() { @@ -38,11 +38,16 @@ } func Main(args []string) { + ctx, err := cmd.DefaultContext() + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(2) + } if err := juju.InitJujuHome(); err != nil { fmt.Fprintf(os.Stderr, "error: %s\n", err) os.Exit(2) } - os.Exit(cmd.Main(&restoreCommand{}, cmd.DefaultContext(), args[1:])) + os.Exit(cmd.Main(&restoreCommand{}, ctx, args[1:])) } var logger = loggo.GetLogger("juju.plugins.restore") @@ -266,11 +271,11 @@ newInstId = instance.Id(info.InstanceId) progress("copying backup file to bootstrap host") - if err := scp(backupFile, addr, "~/juju-backup.tgz"); err != nil { + if err := sendViaScp(backupFile, addr, "~/juju-backup.tgz"); err != nil { return "", "", fmt.Errorf("cannot copy backup file to bootstrap instance: %v", err) } progress("updating bootstrap machine") - if err := ssh(addr, updateBootstrapMachineScript(newInstId, creds)); err != nil { + if err := runViaSsh(addr, updateBootstrapMachineScript(newInstId, creds)); err != nil { return "", "", fmt.Errorf("update script failed: %v", err) } return newInstId, addr, nil @@ -345,13 +350,18 @@ n s/- .*(:[0-9]+)/- {{.Address}}\1/ }" $agent/agent.conf + + # If we're processing a unit agent's directly + # and it has some relations, reset + # the stored version of all of them to + # ensure that any relation hooks will + # fire. if [[ $agent = unit-* ]] then - sed -i -r 's/change-version: [0-9]+$/change-version: 0/' $agent/state/relations/*/* || true + find $agent/state/relations -type f -exec sed -i -r 's/change-version: [0-9]+$/change-version: 0/' {} \; fi initctl start jujud-$agent done -sed -i -r 's/^(:syslogtag, startswith, "juju-" @)(.*)(:[0-9]+.*)$/\1{{.Address}}\3/' /etc/rsyslog.d/*-juju*.conf `) // setAgentAddressScript generates an ssh script argument to update state addresses @@ -404,46 +414,31 @@ if addr == "" { return fmt.Errorf("no appropriate public address found") } - return ssh(addr, sshArg) + return runViaSsh(addr, sshArg) } -func ssh(addr string, script string) error { - args := []string{ - "-l", "ubuntu", - "-T", - "-o", "StrictHostKeyChecking no", - "-o", "PasswordAuthentication no", - addr, - "sudo -n bash -c " + utils.ShQuote(script), - } - cmd := exec.Command("ssh", args...) - logger.Debugf("ssh command: %s %q", cmd.Path, cmd.Args) - data, err := cmd.CombinedOutput() +func runViaSsh(addr string, script string) error { + // This is taken from cmd/juju/ssh.go there is no other clear way to set user + userAddr := "ubuntu@" + addr + cmd := ssh.Command(userAddr, []string{"sudo", "-n", "bash", "-c " + utils.ShQuote(script)}, nil) + var stderrBuf bytes.Buffer + var stdoutBuf bytes.Buffer + cmd.Stderr = &stderrBuf + cmd.Stdout = &stdoutBuf + err := cmd.Run() if err != nil { - return fmt.Errorf("ssh command failed: %v (%q)", err, data) + return fmt.Errorf("ssh command failed: %v (%q)", err, stderrBuf.String()) } - progress("ssh command succeeded: %q", data) + progress("ssh command succedded: %q", stdoutBuf.String()) return nil } -func scp(file, host, destFile string) error { - cmd := exec.Command( - "scp", - "-B", - "-q", - "-o", "StrictHostKeyChecking no", - "-o", "PasswordAuthentication no", - file, - "ubuntu@"+host+":"+destFile) - logger.Debugf("scp command: %s %q", cmd.Path, cmd.Args) - out, err := cmd.CombinedOutput() - if err == nil { - return nil - } - if _, ok := err.(*exec.ExitError); ok { - return fmt.Errorf("scp failed: %s", out) +func sendViaScp(file, host, destFile string) error { + err := ssh.Copy([]string{file, "ubuntu@" + host + ":" + destFile}, nil, nil) + if err != nil { + return fmt.Errorf("scp command failed: %v", err) } - return err + return nil } func mustParseTemplate(templ string) *template.Template { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/local/main.go juju-core-1.17.7/src/launchpad.net/juju-core/cmd/plugins/local/main.go --- juju-core-1.17.6/src/launchpad.net/juju-core/cmd/plugins/local/main.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/cmd/plugins/local/main.go 2014-03-27 15:48:42.000000000 +0000 @@ -37,8 +37,13 @@ // Main registers subcommands for the juju-local executable. func Main(args []string) { + ctx, err := cmd.DefaultContext() + if err != nil { + logger.Debugf("error: %v\n", err) + os.Exit(2) + } plugin := jujuLocalPlugin() - os.Exit(cmd.Main(plugin, cmd.DefaultContext(), args[1:])) + os.Exit(cmd.Main(plugin, ctx, args[1:])) } var checkIfRoot = func() bool { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/downloader/downloader.go juju-core-1.17.7/src/launchpad.net/juju-core/downloader/downloader.go --- juju-core-1.17.6/src/launchpad.net/juju-core/downloader/downloader.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/downloader/downloader.go 2014-03-27 15:48:42.000000000 +0000 @@ -26,19 +26,19 @@ // Download can download a file from the network. type Download struct { - tomb tomb.Tomb - done chan Status - disableSSLHostnameVerification bool + tomb tomb.Tomb + done chan Status + hostnameVerification utils.SSLHostnameVerification } // New returns a new Download instance downloading from the given URL // to the given directory. If dir is empty, it defaults to // os.TempDir(). If disableSSLHostnameVerification is true then a non- // validating http client will be used. -func New(url, dir string, disableSSLHostnameVerification bool) *Download { +func New(url, dir string, hostnameVerification utils.SSLHostnameVerification) *Download { d := &Download{ - done: make(chan Status), - disableSSLHostnameVerification: disableSSLHostnameVerification, + done: make(chan Status), + hostnameVerification: hostnameVerification, } go d.run(url, dir) return d @@ -62,7 +62,7 @@ // TODO(dimitern) 2013-10-03 bug #1234715 // Add a testing HTTPS storage to verify the // disableSSLHostnameVerification behavior here. - file, err := download(url, dir, d.disableSSLHostnameVerification) + file, err := download(url, dir, d.hostnameVerification) if err != nil { err = fmt.Errorf("cannot download %q: %v", url, err) } @@ -77,7 +77,7 @@ } } -func download(url, dir string, disableSSLHostnameVerification bool) (file *os.File, err error) { +func download(url, dir string, hostnameVerification utils.SSLHostnameVerification) (file *os.File, err error) { if dir == "" { dir = os.TempDir() } @@ -91,10 +91,7 @@ } }() // TODO(rog) make the download operation interruptible. - client := http.DefaultClient - if disableSSLHostnameVerification { - client = utils.GetNonValidatingHTTPClient() - } + client := utils.GetHTTPClient(hostnameVerification) resp, err := client.Get(url) if err != nil { return nil, err diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/downloader/downloader_test.go juju-core-1.17.7/src/launchpad.net/juju-core/downloader/downloader_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/downloader/downloader_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/downloader/downloader_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -15,6 +15,7 @@ "launchpad.net/juju-core/downloader" "launchpad.net/juju-core/testing" "launchpad.net/juju-core/testing/testbase" + "launchpad.net/juju-core/utils" ) type suite struct { @@ -48,10 +49,10 @@ gc.TestingT(t) } -func (s *suite) testDownload(c *gc.C, disableSSLHostnameVerification bool) { +func (s *suite) testDownload(c *gc.C, hostnameVerification utils.SSLHostnameVerification) { tmp := c.MkDir() testing.Server.Response(200, nil, []byte("archive")) - d := downloader.New(s.URL("/archive.tgz"), tmp, disableSSLHostnameVerification) + d := downloader.New(s.URL("/archive.tgz"), tmp, hostnameVerification) status := <-d.Done() c.Assert(status.Err, gc.IsNil) c.Assert(status.File, gc.NotNil) @@ -64,16 +65,16 @@ } func (s *suite) TestDownloadWithoutDisablingSSLHostnameVerification(c *gc.C) { - s.testDownload(c, false) + s.testDownload(c, utils.VerifySSLHostnames) } func (s *suite) TestDownloadWithDisablingSSLHostnameVerification(c *gc.C) { - s.testDownload(c, true) + s.testDownload(c, utils.NoVerifySSLHostnames) } func (s *suite) TestDownloadError(c *gc.C) { testing.Server.Response(404, nil, nil) - d := downloader.New(s.URL("/archive.tgz"), c.MkDir(), false) + d := downloader.New(s.URL("/archive.tgz"), c.MkDir(), utils.VerifySSLHostnames) status := <-d.Done() c.Assert(status.File, gc.IsNil) c.Assert(status.Err, gc.ErrorMatches, `cannot download ".*": bad http response: 404 Not Found`) @@ -81,7 +82,7 @@ func (s *suite) TestStopDownload(c *gc.C) { tmp := c.MkDir() - d := downloader.New(s.URL("/x.tgz"), tmp, false) + d := downloader.New(s.URL("/x.tgz"), tmp, utils.VerifySSLHostnames) d.Stop() select { case status := <-d.Done(): diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/bootstrap_test.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/bootstrap/bootstrap_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/bootstrap_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/bootstrap/bootstrap_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -8,6 +8,7 @@ "strings" stdtesting "testing" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/constraints" @@ -216,7 +217,7 @@ s.setDummyStorage(c, env) envtesting.RemoveFakeTools(c, env.Storage()) arch := "ppc64" - _, err := bootstrap.EnsureToolsAvailability(env, env.Config().DefaultSeries(), &arch) + _, err := bootstrap.EnsureToolsAvailability(coretesting.Context(c), env, env.Config().DefaultSeries(), &arch) c.Assert(err, gc.NotNil) stripped := strings.Replace(err.Error(), "\n", "", -1) c.Assert(stripped, @@ -232,7 +233,7 @@ env := newEnviron("foo", useDefaultKeys, nil) s.setDummyStorage(c, env) envtesting.RemoveFakeTools(c, env.Storage()) - _, err := bootstrap.EnsureToolsAvailability(env, env.Config().DefaultSeries(), nil) + _, err := bootstrap.EnsureToolsAvailability(coretesting.Context(c), env, env.Config().DefaultSeries(), nil) c.Assert(err, gc.NotNil) stripped := strings.Replace(err.Error(), "\n", "", -1) c.Assert(stripped, @@ -245,7 +246,7 @@ env := newEnviron("foo", useDefaultKeys, map[string]interface{}{"agent-version": "1.16.0"}) s.setDummyStorage(c, env) envtesting.RemoveFakeTools(c, env.Storage()) - _, err := bootstrap.EnsureToolsAvailability(env, env.Config().DefaultSeries(), nil) + _, err := bootstrap.EnsureToolsAvailability(coretesting.Context(c), env, env.Config().DefaultSeries(), nil) c.Assert(err, gc.NotNil) stripped := strings.Replace(err.Error(), "\n", "", -1) c.Assert(stripped, @@ -259,7 +260,7 @@ env := newEnviron("foo", useDefaultKeys, nil) s.setDummyStorage(c, env) envtesting.RemoveFakeTools(c, env.Storage()) - _, err := bootstrap.EnsureToolsAvailability(env, env.Config().DefaultSeries(), nil) + _, err := bootstrap.EnsureToolsAvailability(coretesting.Context(c), env, env.Config().DefaultSeries(), nil) c.Assert(err, gc.NotNil) stripped := strings.Replace(err.Error(), "\n", "", -1) c.Assert(stripped, @@ -309,7 +310,7 @@ return "arm64" }) arch := "arm64" - agentTools, err := bootstrap.EnsureToolsAvailability(env, env.Config().DefaultSeries(), &arch) + agentTools, err := bootstrap.EnsureToolsAvailability(coretesting.Context(c), env, env.Config().DefaultSeries(), &arch) c.Assert(err, gc.IsNil) c.Assert(agentTools, gc.HasLen, 1) expectedVers := version.Current @@ -324,11 +325,11 @@ s.PatchValue(&version.Current, vers) env := newEnviron("foo", useDefaultKeys, nil) cfg := env.Config() - c.Assert(bootstrap.SeriesToUpload(cfg, nil), gc.DeepEquals, []string{"quantal", "precise"}) - c.Assert(bootstrap.SeriesToUpload(cfg, []string{"quantal"}), gc.DeepEquals, []string{"quantal"}) + c.Assert(bootstrap.SeriesToUpload(cfg, nil), jc.SameContents, []string{"quantal", "precise"}) + c.Assert(bootstrap.SeriesToUpload(cfg, []string{"quantal"}), jc.SameContents, []string{"quantal"}) env = newEnviron("foo", useDefaultKeys, map[string]interface{}{"default-series": "lucid"}) cfg = env.Config() - c.Assert(bootstrap.SeriesToUpload(cfg, nil), gc.DeepEquals, []string{"quantal", "precise", "lucid"}) + c.Assert(bootstrap.SeriesToUpload(cfg, nil), jc.SameContents, []string{"quantal", "precise", "lucid"}) } func (s *bootstrapSuite) assertUploadTools(c *gc.C, vers version.Binary, allowRelease bool, @@ -352,7 +353,7 @@ return "arm64" }) arch := "arm64" - err := bootstrap.UploadTools(env, &arch, allowRelease, "precise") + err := bootstrap.UploadTools(coretesting.Context(c), env, &arch, allowRelease, "precise") if errMessage != "" { stripped := strings.Replace(err.Error(), "\n", "", -1) c.Assert(stripped, gc.Matches, errMessage) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/export_test.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/bootstrap/export_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/export_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/bootstrap/export_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -0,0 +1,8 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package bootstrap + +var ( + NewInterruptibleStorage = newInterruptibleStorage +) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/interruptiblestorage.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/bootstrap/interruptiblestorage.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/interruptiblestorage.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/bootstrap/interruptiblestorage.go 2014-03-27 15:48:42.000000000 +0000 @@ -0,0 +1,62 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package bootstrap + +import ( + "errors" + "io" + + "launchpad.net/juju-core/environs/storage" +) + +var interruptedError = errors.New("interrupted") + +// interruptibleStorage is a storage.Storage that sits +// between the user and another Storage, allowing the +// Put method to be interrupted. +type interruptibleStorage struct { + storage.Storage + interrupt <-chan struct{} +} + +// newInterruptibleStorage wraps the provided Storage so that Put +// will immediately return an error if the provided channel is +// closed. +func newInterruptibleStorage(s storage.Storage, interrupt <-chan struct{}) storage.Storage { + return &interruptibleStorage{s, interrupt} +} + +type interruptibleReader struct { + io.Reader + interrupt <-chan struct{} +} + +func (r *interruptibleReader) Read(p []byte) (int, error) { + // if the interrupt channel is already + // closed, just drop out immediately. + select { + case <-r.interrupt: + return 0, interruptedError + default: + } + + // read and wait for interruption concurrently + var n int + var err error + done := make(chan struct{}) + go func() { + defer close(done) + n, err = r.Reader.Read(p) + }() + select { + case <-done: + return n, err + case <-r.interrupt: + return 0, interruptedError + } +} + +func (s *interruptibleStorage) Put(name string, r io.Reader, length int64) error { + return s.Storage.Put(name, &interruptibleReader{r, s.interrupt}, length) +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/interruptiblestorage_test.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/bootstrap/interruptiblestorage_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/interruptiblestorage_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/bootstrap/interruptiblestorage_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -0,0 +1,74 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package bootstrap_test + +import ( + "fmt" + + gc "launchpad.net/gocheck" + + "launchpad.net/juju-core/environs/bootstrap" + envtesting "launchpad.net/juju-core/environs/testing" + "launchpad.net/juju-core/testing/testbase" +) + +type interruptibleStorageSuite struct { + testbase.LoggingSuite +} + +var _ = gc.Suite(&interruptibleStorageSuite{}) + +type errorReader struct { + close chan struct{} + wait chan struct{} + called int + err error +} + +func (r *errorReader) Read(buf []byte) (int, error) { + if r.close != nil { + close(r.close) + } + if r.wait != nil { + <-r.wait + } + r.called++ + return 0, r.err +} + +func (s *interruptibleStorageSuite) TestInterruptStorage(c *gc.C) { + closer, stor, _ := envtesting.CreateLocalTestStorage(c) + s.AddCleanup(func(c *gc.C) { closer.Close() }) + reader := &errorReader{ + err: fmt.Errorf("read failed"), + } + interrupted := make(chan struct{}) + istor := bootstrap.NewInterruptibleStorage(stor, interrupted) + + err := istor.Put("name", reader, 3) + c.Assert(err, gc.ErrorMatches, ".*: read failed") + c.Assert(reader.called, gc.Equals, 1) + + // If the channel is already closed, then the + // underlying reader is never deferred to. + close(interrupted) + err = istor.Put("name", reader, 3) + c.Assert(err, gc.ErrorMatches, ".*: interrupted") + c.Assert(reader.called, gc.Equals, 1) +} + +func (s *interruptibleStorageSuite) TestInterruptStorageConcurrently(c *gc.C) { + closer, stor, _ := envtesting.CreateLocalTestStorage(c) + s.AddCleanup(func(c *gc.C) { closer.Close() }) + reader := &errorReader{ + close: make(chan struct{}), + wait: make(chan struct{}), + err: fmt.Errorf("read failed"), + } + istor := bootstrap.NewInterruptibleStorage(stor, reader.close) + err := istor.Put("name", reader, 3) + c.Assert(err, gc.ErrorMatches, ".*: interrupted") + c.Assert(reader.called, gc.Equals, 0) // reader is blocked + close(reader.wait) +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/state.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/bootstrap/state.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/state.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/bootstrap/state.go 2014-03-27 15:48:42.000000000 +0000 @@ -8,7 +8,6 @@ "fmt" "io" "io/ioutil" - "net/http" "launchpad.net/goyaml" @@ -16,7 +15,6 @@ "launchpad.net/juju-core/environs/storage" coreerrors "launchpad.net/juju-core/errors" "launchpad.net/juju-core/instance" - "launchpad.net/juju-core/utils" ) // StateFile is the name of the file where the provider's state is stored. @@ -39,24 +37,24 @@ // putState writes the given data to the state file on the given storage. // The file's name is as defined in StateFile. -func putState(storage storage.StorageWriter, data []byte) error { - logger.Debugf("putting %q to bootstrap storage %T", StateFile, storage) - return storage.Put(StateFile, bytes.NewBuffer(data), int64(len(data))) +func putState(stor storage.StorageWriter, data []byte) error { + logger.Debugf("putting %q to bootstrap storage %T", StateFile, stor) + return stor.Put(StateFile, bytes.NewBuffer(data), int64(len(data))) } // CreateStateFile creates an empty state file on the given storage, and // returns its URL. -func CreateStateFile(storage storage.Storage) (string, error) { - err := putState(storage, []byte{}) +func CreateStateFile(stor storage.Storage) (string, error) { + err := putState(stor, []byte{}) if err != nil { return "", fmt.Errorf("cannot create initial state file: %v", err) } - return storage.URL(StateFile) + return stor.URL(StateFile) } // DeleteStateFile deletes the state file on the given storage. -func DeleteStateFile(storage storage.Storage) error { - return storage.Remove(StateFile) +func DeleteStateFile(stor storage.Storage) error { + return stor.Remove(StateFile) } // SaveState writes the given state to the given storage. @@ -68,24 +66,6 @@ return putState(storage, data) } -// LoadStateFromURL reads state from the given URL. -func LoadStateFromURL(url string, disableSSLHostnameVerification bool) (*BootstrapState, error) { - logger.Debugf("loading %q from %q", StateFile, url) - client := http.DefaultClient - if disableSSLHostnameVerification { - logger.Infof("hostname SSL verification disabled") - client = utils.GetNonValidatingHTTPClient() - } - resp, err := client.Get(url) - if err != nil { - return nil, err - } - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("could not load state from url: %v %s", url, resp.Status) - } - return loadState(resp.Body) -} - // LoadState reads state from the given storage. func LoadState(stor storage.StorageReader) (*BootstrapState, error) { r, err := storage.Get(stor, StateFile) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/state_test.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/bootstrap/state_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/state_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/bootstrap/state_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -125,33 +125,6 @@ c.Check(*storedState, gc.DeepEquals, state) } -func (suite *StateSuite) TestLoadStateFromURLReadsStateFile(c *gc.C) { - storage, dataDir := suite.newStorageWithDataDir(c) - state := suite.setUpSavedState(c, dataDir) - url, err := storage.URL(bootstrap.StateFile) - c.Assert(err, gc.IsNil) - storedState, err := bootstrap.LoadStateFromURL(url, false) - c.Assert(err, gc.IsNil) - c.Check(*storedState, gc.DeepEquals, state) -} - -func (suite *StateSuite) TestLoadStateFromURLBadCert(c *gc.C) { - baseURL, _ := suite.testingHTTPSServer(c) - url := baseURL + "/" + bootstrap.StateFile - storedState, err := bootstrap.LoadStateFromURL(url, false) - c.Assert(err, gc.ErrorMatches, ".*/provider-state:.* certificate signed by unknown authority") - c.Assert(storedState, gc.IsNil) -} - -func (suite *StateSuite) TestLoadStateFromURLBadCertPermitted(c *gc.C) { - baseURL, dataDir := suite.testingHTTPSServer(c) - state := suite.setUpSavedState(c, dataDir) - url := baseURL + "/" + bootstrap.StateFile - storedState, err := bootstrap.LoadStateFromURL(url, true) - c.Assert(err, gc.IsNil) - c.Check(*storedState, gc.DeepEquals, state) -} - func (suite *StateSuite) TestLoadStateMissingFile(c *gc.C) { stor := suite.newStorage(c) _, err := bootstrap.LoadState(stor) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/synctools.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/bootstrap/synctools.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/bootstrap/synctools.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/bootstrap/synctools.go 2014-03-27 15:48:42.000000000 +0000 @@ -5,6 +5,7 @@ import ( "fmt" + "os" "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/config" @@ -30,7 +31,7 @@ // the environment storage, after which it sets the agent-version. If forceVersion is true, // we allow uploading release tools versions and allow uploading even when the agent-version is // already set in the environment. -func UploadTools(env environs.Environ, toolsArch *string, forceVersion bool, bootstrapSeries ...string) error { +func UploadTools(ctx environs.BootstrapContext, env environs.Environ, toolsArch *string, forceVersion bool, bootstrapSeries ...string) error { logger.Infof("checking that upload is possible") // Check the series are valid. for _, series := range bootstrapSeries { @@ -43,11 +44,25 @@ return err } + // Make storage interruptible. + interrupted := make(chan os.Signal, 1) + interruptStorage := make(chan struct{}) + ctx.InterruptNotify(interrupted) + defer ctx.StopInterruptNotify(interrupted) + defer close(interrupted) + go func() { + defer close(interruptStorage) // closing interrupts all uploads + if _, ok := <-interrupted; ok { + ctx.Infof("cancelling tools upload") + } + }() + stor := newInterruptibleStorage(env.Storage(), interruptStorage) + cfg := env.Config() explicitVersion := uploadVersion(version.Current.Number, nil) uploadSeries := SeriesToUpload(cfg, bootstrapSeries) - logger.Infof("uploading tools for series %s", uploadSeries) - tools, err := sync.Upload(env.Storage(), &explicitVersion, uploadSeries...) + ctx.Infof("uploading tools for series %s", uploadSeries) + tools, err := sync.Upload(stor, &explicitVersion, uploadSeries...) if err != nil { return err } @@ -131,7 +146,7 @@ // EnsureToolsAvailability verifies the tools are available. If no tools are // found, it will automatically synchronize them. -func EnsureToolsAvailability(env environs.Environ, series string, toolsArch *string) (coretools.List, error) { +func EnsureToolsAvailability(ctx environs.BootstrapContext, env environs.Environ, series string, toolsArch *string) (coretools.List, error) { cfg := env.Config() var vers *version.Number if agentVersion, ok := cfg.AgentVersion(); ok { @@ -160,7 +175,7 @@ // No tools available so our only hope is to build locally and upload. logger.Warningf("no prepackaged tools available") uploadSeries := SeriesToUpload(cfg, nil) - if err := UploadTools(env, toolsArch, false, append(uploadSeries, series)...); err != nil { + if err := UploadTools(ctx, env, toolsArch, false, append(uploadSeries, series)...); err != nil { logger.Errorf("%s", noToolsMessage) return nil, fmt.Errorf("cannot upload bootstrap tools: %v", err) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/broker.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/broker.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/broker.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/broker.go 2014-03-27 15:48:42.000000000 +0000 @@ -10,11 +10,10 @@ "launchpad.net/juju-core/tools" ) -// Networks holds network include/exclude -// configuration +// Networks holds network include/exclude when starting an instance. type Networks struct { - IncludedNetworks []string - ExcludedNetworks []string + IncludeNetworks []string + ExcludeNetworks []string } // StartInstanceParams holds parameters for the @@ -24,8 +23,7 @@ // the kind of instance to create. Constraints constraints.Value - // Networks holds 2 string slices indicating - // respectively included and excluded Networks + // Networks holds networks to include/exclude for the instance. Networks Networks // Tools is a list of tools that may be used diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/cloudinit/cloudinit.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/cloudinit/cloudinit.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/cloudinit/cloudinit.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/cloudinit/cloudinit.go 2014-03-27 15:48:42.000000000 +0000 @@ -31,12 +31,6 @@ "launchpad.net/juju-core/version" ) -// BootstrapStateURLFile is used to communicate to the first bootstrap node -// the URL from which to obtain important state information (instance id and -// hardware characteristics). It is a transient file, only used as the node -// is bootstrapping. -const BootstrapStateURLFile = "/tmp/provider-state-url" - // SystemIdentity is the name of the file where the environment SSH key is kept. const SystemIdentity = "system-identity" @@ -79,6 +73,15 @@ // or be empty when starting a state server. APIInfo *api.Info + // InstanceId is the instance ID of the machine being initialised. + // This is required when bootstrapping, and ignored otherwise. + InstanceId instance.Id + + // HardwareCharacteristics contains the harrdware characteristics of + // the machine being initialised. This optional, and is only used by + // the bootstrap agent during state initialisation. + HardwareCharacteristics *instance.HardwareCharacteristics + // MachineNonce is set at provisioning/bootstrap time and used to // ensure the agent is running on the correct instance. MachineNonce string @@ -128,9 +131,6 @@ // Constraints holds the initial environment constraints. Constraints constraints.Value - // StateInfoURL is the URL of a file which contains information about the state server machines. - StateInfoURL string - // DisableSSLHostnameVerification can be set to true to tell cloud-init // that it shouldn't verify SSL certificates DisableSSLHostnameVerification bool @@ -291,6 +291,7 @@ // defined, and we determine this by the existance of the home dir. fmt.Sprintf("[ -e /home/ubuntu ] && chown ubuntu:ubuntu %s", lockDir), fmt.Sprintf("mkdir -p %s", cfg.LogDir), + fmt.Sprintf("chown syslog:adm %s", cfg.LogDir), ) // Make a directory for the tools to live in, then fetch the @@ -393,13 +394,20 @@ if cons != "" { cons = " --constraints " + shquote(cons) } + var hardware string + if cfg.HardwareCharacteristics != nil { + if hardware = cfg.HardwareCharacteristics.String(); hardware != "" { + hardware = " --hardware " + shquote(hardware) + } + } c.AddRunCmd(cloudinit.LogProgressCmd("Bootstrapping Juju machine agent")) c.AddScripts( - fmt.Sprintf("echo %s > %s", shquote(cfg.StateInfoURL), BootstrapStateURLFile), // The bootstrapping is always run with debug on. cfg.jujuTools()+"/jujud bootstrap-state"+ " --data-dir "+shquote(cfg.DataDir)+ " --env-config "+shquote(base64yaml(cfg.Config))+ + " --instance-id "+shquote(string(cfg.InstanceId))+ + hardware+ cons+ " --debug", "rm -rf "+shquote(acfg.Dir()), @@ -701,6 +709,9 @@ if cfg.SystemPrivateSSHKey == "" { return fmt.Errorf("missing system ssh identity") } + if cfg.InstanceId == "" { + return fmt.Errorf("missing instance-id") + } } else { if len(cfg.StateInfo.Addrs) == 0 { return fmt.Errorf("missing state hosts") diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/cloudinit/cloudinit_test.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/cloudinit/cloudinit_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/cloudinit/cloudinit_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/cloudinit/cloudinit_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -101,7 +101,7 @@ LogDir: agent.DefaultLogDir, Jobs: allMachineJobs, CloudInitOutputLog: environs.CloudInitOutputLog, - StateInfoURL: "some-url", + InstanceId: "i-bootstrap", SystemPrivateSSHKey: "private rsa key", MachineAgentServiceName: "jujud-machine-0", MongoServiceName: "juju-db", @@ -117,6 +117,7 @@ mkdir -p /var/lib/juju/locks \[ -e /home/ubuntu \] && chown ubuntu:ubuntu /var/lib/juju/locks mkdir -p /var/log/juju +chown syslog:adm /var/log/juju echo 'Fetching tools.* bin='/var/lib/juju/tools/1\.2\.3-precise-amd64' mkdir -p \$bin @@ -147,8 +148,7 @@ install -m 600 /dev/null '/var/lib/juju/agents/bootstrap/agent\.conf' printf '%s\\n' '.*' > '/var/lib/juju/agents/bootstrap/agent\.conf' echo 'Bootstrapping Juju machine agent'.* -echo 'some-url' > /tmp/provider-state-url -/var/lib/juju/tools/1\.2\.3-precise-amd64/jujud bootstrap-state --data-dir '/var/lib/juju' --env-config '[^']*' --constraints 'mem=2048M' --debug +/var/lib/juju/tools/1\.2\.3-precise-amd64/jujud bootstrap-state --data-dir '/var/lib/juju' --env-config '[^']*' --instance-id 'i-bootstrap' --constraints 'mem=2048M' --debug rm -rf '/var/lib/juju/agents/bootstrap' ln -s 1\.2\.3-precise-amd64 '/var/lib/juju/tools/machine-0' echo 'Starting Juju machine agent \(jujud-machine-0\)'.* @@ -182,7 +182,7 @@ LogDir: agent.DefaultLogDir, Jobs: allMachineJobs, CloudInitOutputLog: environs.CloudInitOutputLog, - StateInfoURL: "some-url", + InstanceId: "i-bootstrap", SystemPrivateSSHKey: "private rsa key", MachineAgentServiceName: "jujud-machine-0", MongoServiceName: "juju-db", @@ -196,7 +196,7 @@ grep '1234' \$bin/juju1\.2\.3-raring-amd64.sha256 \|\| \(echo "Tools checksum mismatch"; exit 1\) rm \$bin/tools\.tar\.gz && rm \$bin/juju1\.2\.3-raring-amd64\.sha256 printf %s '{"version":"1\.2\.3-raring-amd64","url":"http://foo\.com/tools/releases/juju1\.2\.3-raring-amd64\.tgz","sha256":"1234","size":10}' > \$bin/downloaded-tools\.txt -/var/lib/juju/tools/1\.2\.3-raring-amd64/jujud bootstrap-state --data-dir '/var/lib/juju' --env-config '[^']*' --constraints 'mem=2048M' --debug +/var/lib/juju/tools/1\.2\.3-raring-amd64/jujud bootstrap-state --data-dir '/var/lib/juju' --env-config '[^']*' --instance-id 'i-bootstrap' --constraints 'mem=2048M' --debug rm -rf '/var/lib/juju/agents/bootstrap' ln -s 1\.2\.3-raring-amd64 '/var/lib/juju/tools/machine-0' `, @@ -227,7 +227,7 @@ LogDir: agent.DefaultLogDir, Jobs: allMachineJobs, CloudInitOutputLog: environs.CloudInitOutputLog, - StateInfoURL: "some-url", + InstanceId: "i-bootstrap", SystemPrivateSSHKey: "private rsa key", MachineAgentServiceName: "jujud-machine-0", MongoServiceName: "juju-db", @@ -274,6 +274,7 @@ mkdir -p /var/lib/juju/locks \[ -e /home/ubuntu \] && chown ubuntu:ubuntu /var/lib/juju/locks mkdir -p /var/log/juju +chown syslog:adm /var/log/juju echo 'Fetching tools.* bin='/var/lib/juju/tools/1\.2\.3-linux-amd64' mkdir -p \$bin @@ -386,7 +387,7 @@ LogDir: agent.DefaultLogDir, Jobs: allMachineJobs, CloudInitOutputLog: environs.CloudInitOutputLog, - StateInfoURL: "some-url", + InstanceId: "i-bootstrap", SystemPrivateSSHKey: "private rsa key", MachineAgentServiceName: "jujud-machine-0", MongoServiceName: "juju-db", @@ -394,7 +395,7 @@ setEnvConfig: true, inexactMatch: true, expectScripts: ` -/var/lib/juju/tools/1\.2\.3-precise-amd64/jujud bootstrap-state --data-dir '/var/lib/juju' --env-config '[^']*' --debug +/var/lib/juju/tools/1\.2\.3-precise-amd64/jujud bootstrap-state --data-dir '/var/lib/juju' --env-config '[^']*' --instance-id 'i-bootstrap' --debug `, }, } @@ -783,6 +784,9 @@ {"missing mongo service name", func(cfg *cloudinit.MachineConfig) { cfg.MongoServiceName = "" }}, + {"missing instance-id", func(cfg *cloudinit.MachineConfig) { + cfg.InstanceId = "" + }}, } // TestCloudInitVerify checks that required fields are appropriately @@ -812,6 +816,7 @@ LogDir: agent.DefaultLogDir, Jobs: normalMachineJobs, CloudInitOutputLog: environs.CloudInitOutputLog, + InstanceId: "i-bootstrap", MachineNonce: "FAKE_NONCE", SystemPrivateSSHKey: "private rsa key", MachineAgentServiceName: "jujud-machine-99", diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/cloudinit.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/cloudinit.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/cloudinit.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/cloudinit.go 2014-03-27 15:48:42.000000000 +0000 @@ -57,13 +57,11 @@ // NewBootstrapMachineConfig sets up a basic machine configuration for a // bootstrap node. You'll still need to supply more information, but this // takes care of the fixed entries and the ones that are always needed. -// stateInfoURL is the storage URL for the environment's state file. -func NewBootstrapMachineConfig(stateInfoURL string, privateSystemSSHKey string) *cloudinit.MachineConfig { +func NewBootstrapMachineConfig(privateSystemSSHKey string) *cloudinit.MachineConfig { // For a bootstrap instance, FinishMachineConfig will provide the // state.Info and the api.Info. The machine id must *always* be "0". mcfg := NewMachineConfig("0", state.BootstrapNonce, nil, nil) mcfg.StateServer = true - mcfg.StateInfoURL = stateInfoURL mcfg.SystemPrivateSSHKey = privateSystemSSHKey mcfg.Jobs = []params.MachineJob{params.JobManageEnviron, params.JobHostUnits} return mcfg @@ -156,25 +154,26 @@ return nil } -// ComposeUserData puts together a binary (gzipped) blob of user data. -// The additionalScripts are additional command lines that you need cloudinit -// to run on the instance; they are executed before all other cloud-init -// runcmds. Use with care. -func ComposeUserData(cfg *cloudinit.MachineConfig, additionalScripts ...string) ([]byte, error) { - cloudcfg := coreCloudinit.New() - for _, script := range additionalScripts { - cloudcfg.AddRunCmd(script) - } +func configureCloudinit(mcfg *cloudinit.MachineConfig, cloudcfg *coreCloudinit.Config) error { // When bootstrapping, we only want to apt-get update/upgrade // and setup the SSH keys. The rest we leave to cloudinit/sshinit. - if cfg.StateServer { - if err := cloudinit.ConfigureBasic(cfg, cloudcfg); err != nil { - return nil, err - } - } else { - if err := cloudinit.Configure(cfg, cloudcfg); err != nil { - return nil, err - } + if mcfg.StateServer { + return cloudinit.ConfigureBasic(mcfg, cloudcfg) + } + return cloudinit.Configure(mcfg, cloudcfg) +} + +// ComposeUserData fills out the provided cloudinit configuration structure +// so it is suitable for initialising a machine with the given configuration, +// and then renders it and returns it as a binary (gzipped) blob of user data. +// +// If the provided cloudcfg is nil, a new one will be created internally. +func ComposeUserData(mcfg *cloudinit.MachineConfig, cloudcfg *coreCloudinit.Config) ([]byte, error) { + if cloudcfg == nil { + cloudcfg = coreCloudinit.New() + } + if err := configureCloudinit(mcfg, cloudcfg); err != nil { + return nil, err } data, err := cloudcfg.Render() logger.Tracef("Generated cloud init:\n%s", string(data)) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/cloudinit_test.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/cloudinit_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/cloudinit_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/cloudinit_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -12,6 +12,7 @@ "launchpad.net/juju-core/agent" "launchpad.net/juju-core/cert" + coreCloudinit "launchpad.net/juju-core/cloudinit" "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/cloudinit" @@ -191,8 +192,10 @@ } script1 := "script1" script2 := "script2" - scripts := []string{script1, script2} - result, err := environs.ComposeUserData(cfg, scripts...) + cloudcfg := coreCloudinit.New() + cloudcfg.AddRunCmd(script1) + cloudcfg.AddRunCmd(script2) + result, err := environs.ComposeUserData(cfg, cloudcfg) c.Assert(err, gc.IsNil) unzipped, err := utils.Gunzip(result) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/httpstorage/storage.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/httpstorage/storage.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/httpstorage/storage.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/httpstorage/storage.go 2014-03-27 15:48:42.000000000 +0000 @@ -41,7 +41,7 @@ func Client(addr string) storage.Storage { return &localStorage{ addr: addr, - client: http.DefaultClient, + client: utils.GetValidatingHTTPClient(), } } @@ -146,7 +146,7 @@ // modURL returns a URL that can be used to modify the given storage file. func (s *localStorage) modURL(name string) (string, error) { - if s.client == http.DefaultClient { + if s.authkey == "" { return s.URL(name) } s.httpsBaseURLOnce.Do(func() { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/simplestreams_test.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/imagemetadata/simplestreams_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/simplestreams_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/imagemetadata/simplestreams_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -18,6 +18,7 @@ "launchpad.net/juju-core/environs/jujutest" "launchpad.net/juju-core/environs/simplestreams" sstesting "launchpad.net/juju-core/environs/simplestreams/testing" + "launchpad.net/juju-core/utils" ) var live = flag.Bool("live", false, "Include live simplestreams tests") @@ -67,7 +68,7 @@ gc.Suite(&simplestreamsSuite{ LocalLiveSimplestreamsSuite: sstesting.LocalLiveSimplestreamsSuite{ Source: simplestreams.NewURLDataSource( - "test roundtripper", "test:", simplestreams.VerifySSLHostnames), + "test roundtripper", "test:", utils.VerifySSLHostnames), RequireSigned: false, DataType: imagemetadata.ImageIds, ValidConstraint: imagemetadata.NewImageConstraint(simplestreams.LookupParams{ @@ -85,7 +86,7 @@ func registerLiveSimpleStreamsTests(baseURL string, validImageConstraint simplestreams.LookupConstraint, requireSigned bool) { gc.Suite(&sstesting.LocalLiveSimplestreamsSuite{ - Source: simplestreams.NewURLDataSource("test", baseURL, simplestreams.VerifySSLHostnames), + Source: simplestreams.NewURLDataSource("test", baseURL, utils.VerifySSLHostnames), RequireSigned: requireSigned, DataType: imagemetadata.ImageIds, ValidConstraint: validImageConstraint, @@ -267,7 +268,7 @@ Arches: t.arches, }) // Add invalid datasource and check later that resolveInfo is correct. - invalidSource := simplestreams.NewURLDataSource("invalid", "file://invalid", simplestreams.VerifySSLHostnames) + invalidSource := simplestreams.NewURLDataSource("invalid", "file://invalid", utils.VerifySSLHostnames) images, resolveInfo, err := imagemetadata.Fetch( []simplestreams.DataSource{invalidSource, s.Source}, simplestreams.DefaultIndexPath, imageConstraint, s.RequireSigned) @@ -336,7 +337,7 @@ func init() { testRoundTripper = &jujutest.ProxyRoundTripper{} - simplestreams.RegisterProtocol("signedtest", testRoundTripper) + testRoundTripper.RegisterForScheme("signedtest") } func (s *signedSuite) SetUpSuite(c *gc.C) { @@ -375,7 +376,7 @@ } func (s *signedSuite) TestSignedImageMetadata(c *gc.C) { - signedSource := simplestreams.NewURLDataSource("test", "signedtest://host/signed", simplestreams.VerifySSLHostnames) + signedSource := simplestreams.NewURLDataSource("test", "signedtest://host/signed", utils.VerifySSLHostnames) imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ CloudSpec: simplestreams.CloudSpec{"us-east-1", "https://ec2.us-east-1.amazonaws.com"}, Series: []string{"precise"}, @@ -395,7 +396,7 @@ } func (s *signedSuite) TestSignedImageMetadataInvalidSignature(c *gc.C) { - signedSource := simplestreams.NewURLDataSource("test", "signedtest://host/signed", simplestreams.VerifySSLHostnames) + signedSource := simplestreams.NewURLDataSource("test", "signedtest://host/signed", utils.VerifySSLHostnames) imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ CloudSpec: simplestreams.CloudSpec{"us-east-1", "https://ec2.us-east-1.amazonaws.com"}, Series: []string{"precise"}, diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/urls.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/imagemetadata/urls.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/urls.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/imagemetadata/urls.go 2014-03-27 15:48:42.000000000 +0000 @@ -11,6 +11,7 @@ "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/simplestreams" "launchpad.net/juju-core/environs/storage" + "launchpad.net/juju-core/utils" ) // SupportsCustomSources represents an environment that @@ -27,9 +28,9 @@ var sources []simplestreams.DataSource config := env.Config() if userURL, ok := config.ImageMetadataURL(); ok { - verify := simplestreams.VerifySSLHostnames + verify := utils.VerifySSLHostnames if !config.SSLHostnameVerification() { - verify = simplestreams.NoVerifySSLHostnames + verify = utils.NoVerifySSLHostnames } sources = append(sources, simplestreams.NewURLDataSource("image-metadata-url", userURL, verify)) } @@ -46,7 +47,8 @@ return nil, err } if defaultURL != "" { - sources = append(sources, simplestreams.NewURLDataSource("default cloud images", defaultURL, simplestreams.VerifySSLHostnames)) + sources = append(sources, + simplestreams.NewURLDataSource("default cloud images", defaultURL, utils.VerifySSLHostnames)) } return sources, nil } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/validation_test.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/imagemetadata/validation_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/imagemetadata/validation_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/imagemetadata/validation_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -13,6 +13,7 @@ "launchpad.net/juju-core/environs/imagemetadata" "launchpad.net/juju-core/environs/simplestreams" "launchpad.net/juju-core/testing/testbase" + "launchpad.net/juju-core/utils" ) type ValidateSuite struct { @@ -58,7 +59,7 @@ Endpoint: "some-auth-url", Stream: stream, Sources: []simplestreams.DataSource{ - simplestreams.NewURLDataSource("test", "file://"+metadataPath, simplestreams.VerifySSLHostnames)}, + simplestreams.NewURLDataSource("test", "file://"+metadataPath, utils.VerifySSLHostnames)}, } imageIds, resolveInfo, err := imagemetadata.ValidateImageMetadata(params) c.Assert(err, gc.IsNil) @@ -86,7 +87,7 @@ Endpoint: "some-auth-url", Stream: stream, Sources: []simplestreams.DataSource{ - simplestreams.NewURLDataSource("test", "file://"+s.metadataDir, simplestreams.VerifySSLHostnames)}, + simplestreams.NewURLDataSource("test", "file://"+s.metadataDir, utils.VerifySSLHostnames)}, } _, _, err := imagemetadata.ValidateImageMetadata(params) c.Assert(err, gc.Not(gc.IsNil)) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/instances/image_test.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/instances/image_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/instances/image_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/instances/image_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -12,6 +12,7 @@ "launchpad.net/juju-core/environs/imagemetadata" "launchpad.net/juju-core/environs/simplestreams" "launchpad.net/juju-core/testing/testbase" + "launchpad.net/juju-core/utils" ) type imageSuite struct { @@ -241,7 +242,7 @@ }) imageMeta, err := imagemetadata.GetLatestImageIdMetadata( []byte(jsonImagesContent), - simplestreams.NewURLDataSource("test", "some-url", simplestreams.VerifySSLHostnames), cons) + simplestreams.NewURLDataSource("test", "some-url", utils.VerifySSLHostnames), cons) c.Assert(err, gc.IsNil) var images []Image for _, imageMetadata := range imageMeta { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/interface.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/interface.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/interface.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/interface.go 2014-03-27 15:48:42.000000000 +0000 @@ -183,6 +183,8 @@ GetStdin() io.Reader GetStdout() io.Writer GetStderr() io.Writer + Infof(format string, params ...interface{}) + Verbosef(format string, params ...interface{}) // InterruptNotify starts watching for interrupt signals // on behalf of the caller, sending them to the supplied diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/jujutest/livetests.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/jujutest/livetests.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/jujutest/livetests.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/jujutest/livetests.go 2014-03-27 15:48:42.000000000 +0000 @@ -450,9 +450,9 @@ c.Logf("deploying service") repoDir := c.MkDir() url := coretesting.Charms.ClonedURL(repoDir, mtools0.Version.Series, "dummy") - sch, err := conn.PutCharm(url, &charm.LocalRepository{repoDir}, false) + sch, err := conn.PutCharm(url, &charm.LocalRepository{Path: repoDir}, false) c.Assert(err, gc.IsNil) - svc, err := conn.State.AddService("dummy", "user-admin", sch) + svc, err := conn.State.AddService("dummy", "user-admin", sch, nil, nil) c.Assert(err, gc.IsNil) units, err := juju.AddUnits(conn.State, svc, 1, "") c.Assert(err, gc.IsNil) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/jujutest/tests.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/jujutest/tests.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/jujutest/tests.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/jujutest/tests.go 2014-03-27 15:48:42.000000000 +0000 @@ -258,7 +258,7 @@ var resp *http.Response for a := attempt.Start(); a.Next(); { - resp, err = http.Get(url) + resp, err = utils.GetValidatingHTTPClient().Get(url) c.Assert(err, gc.IsNil) if resp.StatusCode != 404 { break diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/manual/bootstrap.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/manual/bootstrap.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/manual/bootstrap.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/manual/bootstrap.go 2014-03-27 15:48:42.000000000 +0000 @@ -124,8 +124,9 @@ } // Finally, provision the machine agent. - stateFileURL := fmt.Sprintf("file://%s/%s", storageDir, bootstrap.StateFile) - mcfg := environs.NewBootstrapMachineConfig(stateFileURL, privateKey) + mcfg := environs.NewBootstrapMachineConfig(privateKey) + mcfg.InstanceId = BootstrapInstanceId + mcfg.HardwareCharacteristics = args.HardwareCharacteristics if args.DataDir != "" { mcfg.DataDir = args.DataDir } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/open.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/open.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/open.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/open.go 2014-03-27 15:48:42.000000000 +0000 @@ -46,6 +46,17 @@ ConfigFromEnvirons ) +// EmptyConfig indicates the .jenv file is empty. +type EmptyConfig struct { + error +} + +// IsEmptyConfig reports whether err is a EmptyConfig. +func IsEmptyConfig(err error) bool { + _, ok := err.(EmptyConfig) + return ok +} + // ConfigForName returns the configuration for the environment with // the given name from the default environments file. If the name is // blank, the default environment will be used. If the configuration @@ -70,7 +81,7 @@ info, err := store.ReadInfo(name) if err == nil { if len(info.BootstrapConfig()) == 0 { - return nil, ConfigFromNowhere, fmt.Errorf("environment has no bootstrap configuration data") + return nil, ConfigFromNowhere, EmptyConfig{fmt.Errorf("environment has no bootstrap configuration data")} } logger.Debugf("ConfigForName found bootstrap config %#v", info.BootstrapConfig()) cfg, err := config.New(config.NoDefaults, info.BootstrapConfig()) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/simplestreams/datasource.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/simplestreams/datasource.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/simplestreams/datasource.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/simplestreams/datasource.go 2014-03-27 15:48:42.000000000 +0000 @@ -30,33 +30,19 @@ SetAllowRetry(allow bool) } -// SSLHostnameVerification is used as a switch for when a given provider might -// use self-signed credentials and we should not try to verify the hostname on -// the TLS/SSL certificates -type SSLHostnameVerification bool - -const ( - // VerifySSLHostnames ensures we verify the hostname on the certificate - // matches the host we are connecting and is signed - VerifySSLHostnames = SSLHostnameVerification(true) - // NoVerifySSLHostnames informs us to skip verifying the hostname - // matches a valid certificate - NoVerifySSLHostnames = SSLHostnameVerification(false) -) - // A urlDataSource retrieves data from an HTTP URL. type urlDataSource struct { description string baseURL string - hostnameVerification SSLHostnameVerification + hostnameVerification utils.SSLHostnameVerification } // NewURLDataSource returns a new datasource reading from the specified baseURL. -func NewURLDataSource(description, baseURL string, verify SSLHostnameVerification) DataSource { +func NewURLDataSource(description, baseURL string, hostnameVerification utils.SSLHostnameVerification) DataSource { return &urlDataSource{ description: description, baseURL: baseURL, - hostnameVerification: verify, + hostnameVerification: hostnameVerification, } } @@ -83,11 +69,8 @@ // Fetch is defined in simplestreams.DataSource. func (h *urlDataSource) Fetch(path string) (io.ReadCloser, string, error) { dataURL := urlJoin(h.baseURL, path) + client := utils.GetHTTPClient(h.hostnameVerification) // dataURL can be http:// or file:// - client := http.DefaultClient - if !h.hostnameVerification { - client = utils.GetNonValidatingHTTPClient() - } resp, err := client.Get(dataURL) if err != nil { logger.Debugf("Got error requesting %q: %v", dataURL, err) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/simplestreams/datasource_test.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/simplestreams/datasource_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/simplestreams/datasource_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/simplestreams/datasource_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -14,6 +14,7 @@ "launchpad.net/juju-core/environs/imagemetadata" "launchpad.net/juju-core/environs/simplestreams" "launchpad.net/juju-core/environs/simplestreams/testing" + "launchpad.net/juju-core/utils" ) var _ = gc.Suite(&datasourceSuite{}) @@ -24,7 +25,7 @@ } func (s *datasourceSuite) TestFetch(c *gc.C) { - ds := simplestreams.NewURLDataSource("test", "test:", simplestreams.VerifySSLHostnames) + ds := simplestreams.NewURLDataSource("test", "test:", utils.VerifySSLHostnames) rc, url, err := ds.Fetch("streams/v1/tools_metadata.json") c.Assert(err, gc.IsNil) defer rc.Close() @@ -36,7 +37,7 @@ } func (s *datasourceSuite) TestURL(c *gc.C) { - ds := simplestreams.NewURLDataSource("test", "foo", simplestreams.VerifySSLHostnames) + ds := simplestreams.NewURLDataSource("test", "foo", utils.VerifySSLHostnames) url, err := ds.URL("bar") c.Assert(err, gc.IsNil) c.Assert(url, gc.Equals, "foo/bar") @@ -64,7 +65,7 @@ } func (s *datasourceHTTPSSuite) TestNormalClientFails(c *gc.C) { - ds := simplestreams.NewURLDataSource("test", s.Server.URL, simplestreams.VerifySSLHostnames) + ds := simplestreams.NewURLDataSource("test", s.Server.URL, utils.VerifySSLHostnames) url, err := ds.URL("bar") c.Assert(err, gc.IsNil) c.Check(url, gc.Equals, s.Server.URL+"/bar") @@ -76,7 +77,7 @@ } func (s *datasourceHTTPSSuite) TestNonVerifyingClientSucceeds(c *gc.C) { - ds := simplestreams.NewURLDataSource("test", s.Server.URL, simplestreams.NoVerifySSLHostnames) + ds := simplestreams.NewURLDataSource("test", s.Server.URL, utils.NoVerifySSLHostnames) url, err := ds.URL("bar") c.Assert(err, gc.IsNil) c.Check(url, gc.Equals, s.Server.URL+"/bar") diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/simplestreams/simplestreams.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/simplestreams/simplestreams.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/simplestreams/simplestreams.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/simplestreams/simplestreams.go 2014-03-27 15:48:42.000000000 +0000 @@ -15,7 +15,6 @@ "fmt" "io" "io/ioutil" - "net/http" "os" "path" "reflect" @@ -26,6 +25,7 @@ "github.com/juju/loggo" "launchpad.net/juju-core/errors" + "launchpad.net/juju-core/utils" ) var logger = loggo.GetLogger("juju.environs.simplestreams") @@ -375,16 +375,6 @@ return result } -func init() { - RegisterProtocol("file", http.NewFileTransport(http.Dir("/"))) -} - -// RegisterProtocol registers a new protocol with the simplestreams http client. -// Exported for testing. -func RegisterProtocol(scheme string, rt http.RoundTripper) { - http.DefaultTransport.(*http.Transport).RegisterProtocol(scheme, rt) -} - // noMatchingProductsError is used to indicate that metadata files have been located, // but there is no metadata satisfying a product criteria. // It is used to distinguish from the situation where the metadata files could not be found. @@ -552,7 +542,7 @@ source, mirrors, params.DataType, params.MirrorContentId, cloudSpec, requireSigned, params.PublicKey) if err == nil { logger.Debugf("using mirrored products path: %s", path.Join(mirrorInfo.MirrorURL, mirrorInfo.Path)) - indexRef.Source = NewURLDataSource("mirror", mirrorInfo.MirrorURL, VerifySSLHostnames) + indexRef.Source = NewURLDataSource("mirror", mirrorInfo.MirrorURL, utils.VerifySSLHostnames) indexRef.MirroredProductsPath = mirrorInfo.Path } else { logger.Debugf("no mirror information available for %s: %v", cloudSpec, err) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/simplestreams/simplestreams_test.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/simplestreams/simplestreams_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/simplestreams/simplestreams_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/simplestreams/simplestreams_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -15,6 +15,7 @@ "launchpad.net/juju-core/environs/simplestreams" sstesting "launchpad.net/juju-core/environs/simplestreams/testing" coretesting "launchpad.net/juju-core/testing" + "launchpad.net/juju-core/utils" ) func Test(t *testing.T) { @@ -27,7 +28,7 @@ func registerSimpleStreamsTests() { gc.Suite(&simplestreamsSuite{ LocalLiveSimplestreamsSuite: sstesting.LocalLiveSimplestreamsSuite{ - Source: simplestreams.NewURLDataSource("test", "test:", simplestreams.VerifySSLHostnames), + Source: simplestreams.NewURLDataSource("test", "test:", utils.VerifySSLHostnames), RequireSigned: false, DataType: "image-ids", ValidConstraint: sstesting.NewTestConstraint(simplestreams.LookupParams{ @@ -316,7 +317,7 @@ func (s *simplestreamsSuite) TestGetMetadataNoMatching(c *gc.C) { source := &countingSource{ DataSource: simplestreams.NewURLDataSource( - "test", "test:/daily", simplestreams.VerifySSLHostnames, + "test", "test:/daily", utils.VerifySSLHostnames, ), } sources := []simplestreams.DataSource{source, source, source} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/simplestreams/testing/testing.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/simplestreams/testing/testing.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/simplestreams/testing/testing.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/simplestreams/testing/testing.go 2014-03-27 15:48:42.000000000 +0000 @@ -480,7 +480,7 @@ func init() { testRoundTripper = &jujutest.ProxyRoundTripper{} - simplestreams.RegisterProtocol("test", testRoundTripper) + testRoundTripper.RegisterForScheme("test") } type TestDataSuite struct{} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/statepolicy.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/statepolicy.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/statepolicy.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/statepolicy.go 2014-03-27 15:48:42.000000000 +0000 @@ -35,10 +35,5 @@ } func (environStatePolicy) ConfigValidator(providerType string) (state.ConfigValidator, error) { - provider, err := Provider(providerType) - if err != nil { - return nil, err - } - - return provider, nil + return Provider(providerType) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/sync/sync.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/sync/sync.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/sync/sync.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/sync/sync.go 2014-03-27 15:48:42.000000000 +0000 @@ -8,7 +8,6 @@ "fmt" "io" "io/ioutil" - "net/http" "os" "path/filepath" @@ -140,7 +139,7 @@ return nil, err } logger.Infof("using sync tools source: %v", sourceURL) - return simplestreams.NewURLDataSource("sync tools source", sourceURL, simplestreams.VerifySSLHostnames), nil + return simplestreams.NewURLDataSource("sync tools source", sourceURL, utils.VerifySSLHostnames), nil } // copyTools copies a set of tools from the source to the target. @@ -161,7 +160,7 @@ func copyOneToolsPackage(tool *coretools.Tools, dest storage.Storage) error { toolsName := envtools.StorageName(tool.Version) logger.Infof("copying %v", toolsName) - resp, err := http.Get(tool.URL) + resp, err := utils.GetValidatingHTTPClient().Get(tool.URL) if err != nil { return err } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/sync/sync_test.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/sync/sync_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/sync/sync_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/sync/sync_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -32,6 +32,7 @@ coretesting "launchpad.net/juju-core/testing" "launchpad.net/juju-core/testing/testbase" coretools "launchpad.net/juju-core/tools" + "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" ) @@ -395,7 +396,7 @@ // downloadTools downloads the supplied tools and extracts them into a // new directory. func downloadTools(c *gc.C, t *coretools.Tools) string { - resp, err := http.Get(t.URL) + resp, err := utils.GetValidatingHTTPClient().Get(t.URL) c.Assert(err, gc.IsNil) defer resp.Body.Close() cmd := exec.Command("tar", "xz") @@ -408,7 +409,7 @@ // downloadToolsRaw downloads the supplied tools and returns the raw bytes. func downloadToolsRaw(c *gc.C, t *coretools.Tools) []byte { - resp, err := http.Get(t.URL) + resp, err := utils.GetValidatingHTTPClient().Get(t.URL) c.Assert(err, gc.IsNil) defer resp.Body.Close() c.Assert(resp.StatusCode, gc.Equals, http.StatusOK) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/testing/tools.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/testing/tools.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/testing/tools.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/testing/tools.go 2014-03-27 15:48:42.000000000 +0000 @@ -5,7 +5,6 @@ import ( "bytes" - "net/http" "os" "path" "path/filepath" @@ -22,6 +21,7 @@ "launchpad.net/juju-core/state" coretesting "launchpad.net/juju-core/testing" coretools "launchpad.net/juju-core/tools" + "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" "launchpad.net/juju-core/worker/upgrader" ) @@ -84,7 +84,7 @@ c.Assert(err, gc.IsNil) agentTools, err := uploadFakeToolsVersion(stor, vers) c.Assert(err, gc.IsNil) - resp, err := http.Get(agentTools.URL) + resp, err := utils.GetValidatingHTTPClient().Get(agentTools.URL) c.Assert(err, gc.IsNil) defer resp.Body.Close() err = agenttools.UnpackTools(dataDir, agentTools, resp.Body) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/tools/simplestreams_test.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/tools/simplestreams_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/tools/simplestreams_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/tools/simplestreams_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -25,6 +25,7 @@ ttesting "launchpad.net/juju-core/environs/tools/testing" "launchpad.net/juju-core/testing/testbase" coretools "launchpad.net/juju-core/tools" + "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" ) @@ -74,7 +75,7 @@ func registerSimpleStreamsTests() { gc.Suite(&simplestreamsSuite{ LocalLiveSimplestreamsSuite: sstesting.LocalLiveSimplestreamsSuite{ - Source: simplestreams.NewURLDataSource("test", "test:", simplestreams.VerifySSLHostnames), + Source: simplestreams.NewURLDataSource("test", "test:", utils.VerifySSLHostnames), RequireSigned: false, DataType: tools.ContentDownload, ValidConstraint: tools.NewVersionedToolsConstraint(version.MustParse("1.13.0"), simplestreams.LookupParams{ @@ -92,7 +93,7 @@ func registerLiveSimpleStreamsTests(baseURL string, validToolsConstraint simplestreams.LookupConstraint, requireSigned bool) { gc.Suite(&sstesting.LocalLiveSimplestreamsSuite{ - Source: simplestreams.NewURLDataSource("test", baseURL, simplestreams.VerifySSLHostnames), + Source: simplestreams.NewURLDataSource("test", baseURL, utils.VerifySSLHostnames), RequireSigned: requireSigned, DataType: tools.ContentDownload, ValidConstraint: validToolsConstraint, @@ -246,7 +247,7 @@ }) } // Add invalid datasource and check later that resolveInfo is correct. - invalidSource := simplestreams.NewURLDataSource("invalid", "file://invalid", simplestreams.VerifySSLHostnames) + invalidSource := simplestreams.NewURLDataSource("invalid", "file://invalid", utils.VerifySSLHostnames) tools, resolveInfo, err := tools.Fetch( []simplestreams.DataSource{invalidSource, s.Source}, simplestreams.DefaultIndexPath, toolsConstraint, s.RequireSigned) @@ -729,7 +730,7 @@ func init() { testRoundTripper = &jujutest.ProxyRoundTripper{} - simplestreams.RegisterProtocol("signedtest", testRoundTripper) + testRoundTripper.RegisterForScheme("signedtest") } func (s *signedSuite) SetUpSuite(c *gc.C) { @@ -768,7 +769,7 @@ } func (s *signedSuite) TestSignedToolsMetadata(c *gc.C) { - signedSource := simplestreams.NewURLDataSource("test", "signedtest://host/signed", simplestreams.VerifySSLHostnames) + signedSource := simplestreams.NewURLDataSource("test", "signedtest://host/signed", utils.VerifySSLHostnames) toolsConstraint := tools.NewVersionedToolsConstraint(version.MustParse("1.13.0"), simplestreams.LookupParams{ CloudSpec: simplestreams.CloudSpec{"us-east-1", "https://ec2.us-east-1.amazonaws.com"}, Series: []string{"precise"}, diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/tools/urls.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/tools/urls.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/tools/urls.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/tools/urls.go 2014-03-27 15:48:42.000000000 +0000 @@ -11,6 +11,7 @@ "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/simplestreams" "launchpad.net/juju-core/environs/storage" + "launchpad.net/juju-core/utils" ) // SupportsCustomSources represents an environment that @@ -35,9 +36,9 @@ var sources []simplestreams.DataSource config := env.Config() if userURL, ok := config.ToolsURL(); ok { - verify := simplestreams.VerifySSLHostnames + verify := utils.VerifySSLHostnames if !config.SSLHostnameVerification() { - verify = simplestreams.NoVerifySSLHostnames + verify = utils.NoVerifySSLHostnames } sources = append(sources, simplestreams.NewURLDataSource("tools-metadata-url", userURL, verify)) } @@ -54,7 +55,8 @@ return nil, err } if defaultURL != "" { - sources = append(sources, simplestreams.NewURLDataSource("default simplestreams", defaultURL, simplestreams.VerifySSLHostnames)) + sources = append(sources, + simplestreams.NewURLDataSource("default simplestreams", defaultURL, utils.VerifySSLHostnames)) } for _, source := range sources { source.SetAllowRetry(allowRetry) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/environs/tools/validation_test.go juju-core-1.17.7/src/launchpad.net/juju-core/environs/tools/validation_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/environs/tools/validation_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/environs/tools/validation_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -11,6 +11,7 @@ "launchpad.net/juju-core/environs/filestorage" "launchpad.net/juju-core/environs/simplestreams" "launchpad.net/juju-core/testing/testbase" + "launchpad.net/juju-core/utils" ) type ValidateSuite struct { @@ -57,7 +58,7 @@ Architectures: []string{"amd64"}, Endpoint: "some-auth-url", Sources: []simplestreams.DataSource{ - simplestreams.NewURLDataSource("test", s.toolsURL(), simplestreams.VerifySSLHostnames)}, + simplestreams.NewURLDataSource("test", s.toolsURL(), utils.VerifySSLHostnames)}, }, } versions, resolveInfo, err := ValidateToolsMetadata(params) @@ -82,7 +83,7 @@ Architectures: []string{"amd64"}, Endpoint: "some-auth-url", Sources: []simplestreams.DataSource{ - simplestreams.NewURLDataSource("test", s.toolsURL(), simplestreams.VerifySSLHostnames)}, + simplestreams.NewURLDataSource("test", s.toolsURL(), utils.VerifySSLHostnames)}, }, } versions, resolveInfo, err := ValidateToolsMetadata(params) @@ -107,7 +108,7 @@ Architectures: []string{"amd64"}, Endpoint: "some-auth-url", Sources: []simplestreams.DataSource{ - simplestreams.NewURLDataSource("test", s.toolsURL(), simplestreams.VerifySSLHostnames)}, + simplestreams.NewURLDataSource("test", s.toolsURL(), utils.VerifySSLHostnames)}, }, } versions, resolveInfo, err := ValidateToolsMetadata(params) @@ -131,7 +132,7 @@ Architectures: []string{"amd64"}, Endpoint: "some-auth-url", Sources: []simplestreams.DataSource{ - simplestreams.NewURLDataSource("test", s.toolsURL(), simplestreams.VerifySSLHostnames)}, + simplestreams.NewURLDataSource("test", s.toolsURL(), utils.VerifySSLHostnames)}, }, } _, _, err := ValidateToolsMetadata(params) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/instance/instance.go juju-core-1.17.7/src/launchpad.net/juju-core/instance/instance.go --- juju-core-1.17.6/src/launchpad.net/juju-core/instance/instance.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/instance/instance.go 2014-03-27 15:48:42.000000000 +0000 @@ -74,12 +74,12 @@ // HardwareCharacteristics represents the characteristics of the instance (if known). // Attributes that are nil are unknown or not supported. type HardwareCharacteristics struct { - Arch *string `yaml:"arch,omitempty"` - Mem *uint64 `yaml:"mem,omitempty"` - RootDisk *uint64 `yaml:"rootdisk,omitempty"` - CpuCores *uint64 `yaml:"cpucores,omitempty"` - CpuPower *uint64 `yaml:"cpupower,omitempty"` - Tags *[]string `yaml:"tags,omitempty"` + Arch *string `json:",omitempty" yaml:"arch,omitempty"` + Mem *uint64 `json:",omitempty" yaml:"mem,omitempty"` + RootDisk *uint64 `json:",omitempty" yaml:"rootdisk,omitempty"` + CpuCores *uint64 `json:",omitempty" yaml:"cpucores,omitempty"` + CpuPower *uint64 `json:",omitempty" yaml:"cpupower,omitempty"` + Tags *[]string `json:",omitempty" yaml:"tags,omitempty"` } func uintStr(i uint64) string { @@ -112,6 +112,16 @@ return strings.Join(strs, " ") } +// Implement gnuflag.Value +func (hc *HardwareCharacteristics) Set(s string) error { + parsed, err := ParseHardware(s) + if err != nil { + return err + } + *hc = parsed + return nil +} + // MustParseHardware constructs a HardwareCharacteristics from the supplied arguments, // as Parse, but panics on failure. func MustParseHardware(args ...string) HardwareCharacteristics { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/juju/conn_test.go juju-core-1.17.7/src/launchpad.net/juju-core/juju/conn_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/juju/conn_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/juju/conn_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -337,10 +337,12 @@ // Invent a URL that points to the bundled charm, and // test putting that. curl := &charm.URL{ - Schema: "local", - Series: "quantal", - Name: "riak", - Revision: -1, + Reference: charm.Reference{ + Schema: "local", + Name: "riak", + Revision: -1, + }, + Series: "quantal", } _, err = s.conn.PutCharm(curl, s.repo, true) c.Assert(err, gc.ErrorMatches, `cannot increment revision of charm "local:quantal/riak-7": not a directory`) @@ -356,7 +358,7 @@ } func (s *ConnSuite) TestPutCharmUpload(c *gc.C) { - repo := &charm.LocalRepository{c.MkDir()} + repo := &charm.LocalRepository{Path: c.MkDir()} curl := coretesting.Charms.ClonedURL(repo.Path, "quantal", "riak") // Put charm for the first time. @@ -396,15 +398,30 @@ c.Assert(sch.Revision(), gc.Equals, rev+1) } +func (s *ConnSuite) assertAssignedMachineNetworks(c *gc.C, unit *state.Unit, expectInclude, expectExclude []string) { + machineId, err := unit.AssignedMachineId() + c.Assert(err, gc.IsNil) + machine, err := s.conn.State.Machine(machineId) + c.Assert(err, gc.IsNil) + include, exclude, err := machine.Networks() + c.Assert(err, gc.IsNil) + c.Assert(include, jc.DeepEquals, expectInclude) + c.Assert(exclude, jc.DeepEquals, expectExclude) +} + func (s *ConnSuite) TestAddUnits(c *gc.C) { + withNets := []string{"net1", "net2"} + withoutNets := []string{"net3", "net4"} curl := coretesting.Charms.ClonedURL(s.repo.Path, "quantal", "riak") sch, err := s.conn.PutCharm(curl, s.repo, false) c.Assert(err, gc.IsNil) - svc, err := s.conn.State.AddService("testriak", "user-admin", sch) + svc, err := s.conn.State.AddService("testriak", "user-admin", sch, withNets, withoutNets) c.Assert(err, gc.IsNil) units, err := juju.AddUnits(s.conn.State, svc, 2, "") c.Assert(err, gc.IsNil) c.Assert(units, gc.HasLen, 2) + s.assertAssignedMachineNetworks(c, units[0], withNets, withoutNets) + s.assertAssignedMachineNetworks(c, units[1], withNets, withoutNets) id0, err := units[0].AssignedMachineId() c.Assert(err, gc.IsNil) @@ -417,16 +434,19 @@ units, err = juju.AddUnits(s.conn.State, svc, 1, "0") c.Assert(err, gc.IsNil) + s.assertAssignedMachineNetworks(c, units[0], withNets, withoutNets) id2, err := units[0].AssignedMachineId() c.Assert(id2, gc.Equals, id0) units, err = juju.AddUnits(s.conn.State, svc, 1, "lxc:0") c.Assert(err, gc.IsNil) + s.assertAssignedMachineNetworks(c, units[0], withNets, withoutNets) id3, err := units[0].AssignedMachineId() c.Assert(id3, gc.Equals, id0+"/lxc/0") units, err = juju.AddUnits(s.conn.State, svc, 1, "lxc:"+id3) c.Assert(err, gc.IsNil) + s.assertAssignedMachineNetworks(c, units[0], withNets, withoutNets) id4, err := units[0].AssignedMachineId() c.Assert(id4, gc.Equals, id0+"/lxc/0/lxc/0") diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/juju/deploy.go juju-core-1.17.7/src/launchpad.net/juju-core/juju/deploy.go --- juju-core-1.17.6/src/launchpad.net/juju-core/juju/deploy.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/juju/deploy.go 2014-03-27 15:48:42.000000000 +0000 @@ -28,6 +28,10 @@ // - a new container on an existing machine eg "lxc:1" // Use string to avoid ambiguity around machine 0. ToMachineSpec string + // IncludeNetworks holds a list of networks to start on boot. + IncludeNetworks []string + // ExcludeNetworks holds a list of networks to disable on boot. + ExcludeNetworks []string } // DeployService takes a charm and various parameters and deploys it. @@ -49,7 +53,13 @@ } // TODO(fwereade): transactional State.AddService including settings, constraints // (minimumUnitCount, initialMachineIds?). - service, err := st.AddService(args.ServiceName, "user-admin", args.Charm) + service, err := st.AddService( + args.ServiceName, + "user-admin", + args.Charm, + args.IncludeNetworks, + args.ExcludeNetworks, + ) if err != nil { return nil, err } @@ -80,6 +90,11 @@ units := make([]*state.Unit, n) // Hard code for now till we implement a different approach. policy := state.AssignCleanEmpty + // All units should have the same networks as the service. + includeNetworks, excludeNetworks, err := svc.Networks() + if err != nil { + return nil, fmt.Errorf("cannot get service %q networks: %v", svc.Name(), err) + } // TODO what do we do if we fail half-way through this process? for i := 0; i < n; i++ { unit, err := svc.AddUnit() @@ -115,9 +130,11 @@ // Create the new machine marked as dirty so that // nothing else will grab it before we assign the unit to it. template := state.MachineTemplate{ - Series: unit.Series(), - Jobs: []state.MachineJob{state.JobHostUnits}, - Dirty: true, + Series: unit.Series(), + Jobs: []state.MachineJob{state.JobHostUnits}, + Dirty: true, + IncludeNetworks: includeNetworks, + ExcludeNetworks: excludeNetworks, } m, err = st.AddMachineInsideMachine(template, mid, containerType) } else { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/juju/testing/conn.go juju-core-1.17.7/src/launchpad.net/juju-core/juju/testing/conn.go --- juju-core-1.17.6/src/launchpad.net/juju-core/juju/testing/conn.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/juju/testing/conn.go 2014-03-27 15:48:42.000000000 +0000 @@ -293,8 +293,12 @@ } func (s *JujuConnSuite) AddTestingService(c *gc.C, name string, ch *state.Charm) *state.Service { + return s.AddTestingServiceWithNetworks(c, name, ch, nil, nil) +} + +func (s *JujuConnSuite) AddTestingServiceWithNetworks(c *gc.C, name string, ch *state.Charm, includeNetworks, excludeNetworks []string) *state.Service { c.Assert(s.State, gc.NotNil) - service, err := s.State.AddService(name, "user-admin", ch) + service, err := s.State.AddService(name, "user-admin", ch, includeNetworks, excludeNetworks) c.Assert(err, gc.IsNil) return service } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/customdata.go juju-core-1.17.7/src/launchpad.net/juju-core/provider/azure/customdata.go --- juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/customdata.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/provider/azure/customdata.go 2014-03-27 15:48:42.000000000 +0000 @@ -14,7 +14,7 @@ // makeCustomData produces custom data for Azure. This is a base64-encoded // zipfile of cloudinit userdata. func makeCustomData(cfg *cloudinit.MachineConfig) (string, error) { - zipData, err := environs.ComposeUserData(cfg) + zipData, err := environs.ComposeUserData(cfg, nil) if err != nil { return "", fmt.Errorf("failure while generating custom data: %v", err) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/customdata_test.go juju-core-1.17.7/src/launchpad.net/juju-core/provider/azure/customdata_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/customdata_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/provider/azure/customdata_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -73,7 +73,7 @@ data, err := base64.StdEncoding.DecodeString(encodedData) c.Assert(err, gc.IsNil) - reference, err := environs.ComposeUserData(cfg) + reference, err := environs.ComposeUserData(cfg, nil) c.Assert(err, gc.IsNil) c.Check(data, gc.DeepEquals, reference) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/environ.go juju-core-1.17.7/src/launchpad.net/juju-core/provider/azure/environ.go --- juju-core-1.17.6/src/launchpad.net/juju-core/provider/azure/environ.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/provider/azure/environ.go 2014-03-27 15:48:42.000000000 +0000 @@ -23,6 +23,7 @@ "launchpad.net/juju-core/provider/common" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" + "launchpad.net/juju-core/utils" "launchpad.net/juju-core/utils/parallel" ) @@ -879,7 +880,7 @@ sources := make([]simplestreams.DataSource, 1+len(baseURLs)) sources[0] = storage.NewStorageSimpleStreamsDataSource("cloud storage", env.Storage(), storage.BaseImagesPath) for i, url := range baseURLs { - sources[i+1] = simplestreams.NewURLDataSource("Azure base URL", url, simplestreams.VerifySSLHostnames) + sources[i+1] = simplestreams.NewURLDataSource("Azure base URL", url, utils.VerifySSLHostnames) } return sources, nil } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/provider/common/bootstrap.go juju-core-1.17.7/src/launchpad.net/juju-core/provider/common/bootstrap.go --- juju-core-1.17.6/src/launchpad.net/juju-core/provider/common/bootstrap.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/provider/common/bootstrap.go 2014-03-27 15:48:42.000000000 +0000 @@ -42,7 +42,7 @@ defer func() { handleBootstrapError(err, ctx, inst, env) }() // First thing, ensure we have tools otherwise there's no point. - selectedTools, err := EnsureBootstrapTools(env, env.Config().DefaultSeries(), cons.Arch) + selectedTools, err := EnsureBootstrapTools(ctx, env, env.Config().DefaultSeries(), cons.Arch) if err != nil { return err } @@ -56,19 +56,11 @@ return fmt.Errorf("no SSH client available") } - // Create an empty bootstrap state file so we can get its URL. - // It will be updated with the instance id and hardware characteristics - // after the bootstrap instance is started. - stateFileURL, err := bootstrap.CreateStateFile(env.Storage()) - if err != nil { - return err - } - privateKey, err := GenerateSystemSSHKey(env) if err != nil { return err } - machineConfig := environs.NewBootstrapMachineConfig(stateFileURL, privateKey) + machineConfig := environs.NewBootstrapMachineConfig(privateKey) fmt.Fprintln(ctx.GetStderr(), "Launching instance") inst, hw, err := env.StartInstance(environs.StartInstanceParams{ @@ -80,6 +72,8 @@ return fmt.Errorf("cannot start bootstrap instance: %v", err) } fmt.Fprintf(ctx.GetStderr(), " - %s\n", inst.Id()) + machineConfig.InstanceId = inst.Id() + machineConfig.HardwareCharacteristics = hw var characteristics []instance.HardwareCharacteristics if hw != nil { @@ -409,8 +403,8 @@ // EnsureBootstrapTools finds tools, syncing with an external tools source as // necessary; it then selects the newest tools to bootstrap with, and sets // agent-version. -func EnsureBootstrapTools(env environs.Environ, series string, arch *string) (coretools.List, error) { - possibleTools, err := bootstrap.EnsureToolsAvailability(env, series, arch) +func EnsureBootstrapTools(ctx environs.BootstrapContext, env environs.Environ, series string, arch *string) (coretools.List, error) { + possibleTools, err := bootstrap.EnsureToolsAvailability(ctx, env, series, arch) if err != nil { return nil, err } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/provider/common/bootstrap_test.go juju-core-1.17.7/src/launchpad.net/juju-core/provider/common/bootstrap_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/provider/common/bootstrap_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/provider/common/bootstrap_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -15,7 +15,6 @@ "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/environs" - "launchpad.net/juju-core/environs/bootstrap" "launchpad.net/juju-core/environs/cloudinit" "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/environs/storage" @@ -76,21 +75,7 @@ return func() *config.Config { return cfg } } -func (s *BootstrapSuite) TestCannotWriteStateFile(c *gc.C) { - brokenStorage := &mockStorage{ - Storage: newStorage(s, c), - putErr: fmt.Errorf("noes!"), - } - env := &mockEnviron{storage: brokenStorage, config: configGetter(c)} - ctx := coretesting.Context(c) - err := common.Bootstrap(ctx, env, constraints.Value{}) - c.Assert(err, gc.ErrorMatches, "cannot create initial state file: noes!") -} - func (s *BootstrapSuite) TestCannotStartInstance(c *gc.C) { - stor := newStorage(s, c) - checkURL, err := stor.URL(bootstrap.StateFile) - c.Assert(err, gc.IsNil) checkCons := constraints.MustParse("mem=8G") startInstance := func( @@ -99,18 +84,18 @@ instance.Instance, *instance.HardwareCharacteristics, error, ) { c.Assert(cons, gc.DeepEquals, checkCons) - c.Assert(mcfg, gc.DeepEquals, environs.NewBootstrapMachineConfig(checkURL, mcfg.SystemPrivateSSHKey)) + c.Assert(mcfg, gc.DeepEquals, environs.NewBootstrapMachineConfig(mcfg.SystemPrivateSSHKey)) return nil, nil, fmt.Errorf("meh, not started") } env := &mockEnviron{ - storage: stor, + storage: newStorage(s, c), startInstance: startInstance, config: configGetter(c), } ctx := coretesting.Context(c) - err = common.Bootstrap(ctx, env, checkCons) + err := common.Bootstrap(ctx, env, checkCons) c.Assert(err, gc.ErrorMatches, "cannot start bootstrap instance: meh, not started") } @@ -192,13 +177,11 @@ checkInstanceId := "i-success" checkHardware := instance.MustParseHardware("mem=2T") - checkURL := "" startInstance := func( _ constraints.Value, _ environs.Networks, _ tools.List, mcfg *cloudinit.MachineConfig, ) ( instance.Instance, *instance.HardwareCharacteristics, error, ) { - checkURL = mcfg.StateInfoURL return &mockInstance{id: checkInstanceId}, &checkHardware, nil } var mocksConfig = minimalConfig(c) @@ -226,12 +209,6 @@ err := common.Bootstrap(ctx, env, constraints.Value{}) c.Assert(err, gc.IsNil) - savedState, err := bootstrap.LoadStateFromURL(checkURL, false) - c.Assert(err, gc.IsNil) - c.Assert(savedState, gc.DeepEquals, &bootstrap.BootstrapState{ - StateInstances: []instance.Id{instance.Id(checkInstanceId)}, - Characteristics: []instance.HardwareCharacteristics{checkHardware}, - }) authKeys := env.Config().AuthorizedKeys() c.Assert(authKeys, gc.Not(gc.Equals), originalAuthKeys) c.Assert(authKeys, jc.HasSuffix, "juju-system-key\n") diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/provider/dummy/environs.go juju-core-1.17.7/src/launchpad.net/juju-core/provider/dummy/environs.go --- juju-core-1.17.6/src/launchpad.net/juju-core/provider/dummy/environs.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/provider/dummy/environs.go 2014-03-27 15:48:42.000000000 +0000 @@ -543,7 +543,7 @@ } func (e *environ) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error { - selectedTools, err := common.EnsureBootstrapTools(e, e.Config().DefaultSeries(), cons.Arch) + selectedTools, err := common.EnsureBootstrapTools(ctx, e, e.Config().DefaultSeries(), cons.Arch) if err != nil { return err } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/provider/ec2/ec2.go juju-core-1.17.7/src/launchpad.net/juju-core/provider/ec2/ec2.go --- juju-core-1.17.6/src/launchpad.net/juju-core/provider/ec2/ec2.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/provider/ec2/ec2.go 2014-03-27 15:48:42.000000000 +0000 @@ -424,7 +424,7 @@ return nil, nil, err } - userData, err := environs.ComposeUserData(args.MachineConfig) + userData, err := environs.ComposeUserData(args.MachineConfig, nil) if err != nil { return nil, nil, fmt.Errorf("cannot make user data: %v", err) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/provider/ec2/image_test.go juju-core-1.17.7/src/launchpad.net/juju-core/provider/ec2/image_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/provider/ec2/image_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/provider/ec2/image_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -11,6 +11,7 @@ "launchpad.net/juju-core/environs/instances" "launchpad.net/juju-core/environs/simplestreams" "launchpad.net/juju-core/testing/testbase" + "launchpad.net/juju-core/utils" ) type imageSuite struct { @@ -129,7 +130,7 @@ stor := ebsStorage spec, err := findInstanceSpec( []simplestreams.DataSource{ - simplestreams.NewURLDataSource("test", "test:", simplestreams.VerifySSLHostnames)}, + simplestreams.NewURLDataSource("test", "test:", utils.VerifySSLHostnames)}, "released", &instances.InstanceConstraint{ Region: "test", @@ -171,7 +172,7 @@ c.Logf("test %d", i) _, err := findInstanceSpec( []simplestreams.DataSource{ - simplestreams.NewURLDataSource("test", "test:", simplestreams.VerifySSLHostnames)}, + simplestreams.NewURLDataSource("test", "test:", utils.VerifySSLHostnames)}, "released", &instances.InstanceConstraint{ Region: "test", diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/provider/ec2/local_test.go juju-core-1.17.7/src/launchpad.net/juju-core/provider/ec2/local_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/provider/ec2/local_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/provider/ec2/local_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -409,7 +409,7 @@ env := t.Prepare(c) a, err := env.SupportedArchitectures() c.Assert(err, gc.IsNil) - c.Assert(a, gc.DeepEquals, []string{"amd64", "i386"}) + c.Assert(a, jc.SameContents, []string{"amd64", "i386"}) } // localNonUSEastSuite is similar to localServerSuite but the S3 mock server diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/config.go juju-core-1.17.7/src/launchpad.net/juju-core/provider/local/config.go --- juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/config.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/provider/local/config.go 2014-03-27 15:48:42.000000000 +0000 @@ -8,6 +8,7 @@ "os" "path/filepath" + "launchpad.net/juju-core/agent" "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/instance" "launchpad.net/juju-core/schema" @@ -84,7 +85,7 @@ } func (c *environConfig) logDir() string { - return filepath.Join(c.rootDir(), "log") + return fmt.Sprintf("%s-%s", agent.DefaultLogDir, c.namespace()) } // bootstrapIPAddress returns the IP address of the bootstrap machine. diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/config_test.go juju-core-1.17.7/src/launchpad.net/juju-core/provider/local/config_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/config_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/provider/local/config_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -106,8 +106,7 @@ } func (s *configSuite) TestBootstrapAsRoot(c *gc.C) { - restore := local.SetRootCheckFunction(func() bool { return true }) - defer restore() + s.PatchValue(local.CheckIfRoot, func() bool { return true }) env, err := local.Provider.Prepare(testing.Context(c), minimalConfig(c)) c.Assert(err, gc.IsNil) err = env.Bootstrap(testing.Context(c), constraints.Value{}) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/environ.go juju-core-1.17.7/src/launchpad.net/juju-core/provider/local/environ.go --- juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/environ.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/provider/local/environ.go 2014-03-27 15:48:42.000000000 +0000 @@ -39,6 +39,7 @@ "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" "launchpad.net/juju-core/state/api/params" + "launchpad.net/juju-core/upstart" "launchpad.net/juju-core/utils/shell" "launchpad.net/juju-core/version" "launchpad.net/juju-core/worker/terminationworker" @@ -109,10 +110,6 @@ // Before we write the agent config file, we need to make sure the // instance is saved in the StateInfo. - stateFileURL, err := bootstrap.CreateStateFile(env.Storage()) - if err != nil { - return err - } if err := bootstrap.SaveState(env.Storage(), &bootstrap.BootstrapState{ StateInstances: []instance.Id{bootstrapInstanceId}, }); err != nil { @@ -121,7 +118,7 @@ } vers := version.Current - selectedTools, err := common.EnsureBootstrapTools(env, vers.Series, &vers.Arch) + selectedTools, err := common.EnsureBootstrapTools(ctx, env, vers.Series, &vers.Arch) if err != nil { return err } @@ -138,12 +135,13 @@ return err } - mcfg := environs.NewBootstrapMachineConfig(stateFileURL, privateKey) + mcfg := environs.NewBootstrapMachineConfig(privateKey) + mcfg.InstanceId = bootstrapInstanceId mcfg.Tools = selectedTools[0] mcfg.DataDir = env.config.rootDir() mcfg.LogDir = fmt.Sprintf("/var/log/juju-%s", env.config.namespace()) mcfg.Jobs = []params.MachineJob{params.JobManageEnviron} - mcfg.CloudInitOutputLog = filepath.Join(env.config.logDir(), "cloud-init-output.log") + mcfg.CloudInitOutputLog = filepath.Join(mcfg.DataDir, "cloud-init-output.log") mcfg.DisablePackageCommands = true mcfg.MachineAgentServiceName = env.machineAgentServiceName() mcfg.MongoServiceName = env.mongoServiceName() @@ -164,15 +162,20 @@ // Also, we leave the old all-machines.log file in // /var/log/juju-{{namespace}} until we start the environment again. So // potentially remove it at the start of the cloud-init. - os.RemoveAll(env.config.logDir()) - os.MkdirAll(env.config.logDir(), 0755) + localLogDir := filepath.Join(mcfg.DataDir, "log") + if err := os.RemoveAll(localLogDir); err != nil { + return err + } + if err := os.Symlink(mcfg.LogDir, localLogDir); err != nil { + return err + } + if err := os.Remove(mcfg.CloudInitOutputLog); err != nil && !os.IsNotExist(err) { + return err + } cloudcfg.AddScripts( fmt.Sprintf("rm -fr %s", mcfg.LogDir), - fmt.Sprintf("mkdir -p %s", mcfg.LogDir), - fmt.Sprintf("chown syslog:adm %s", mcfg.LogDir), fmt.Sprintf("rm -f /var/spool/rsyslog/machine-0-%s", env.config.namespace()), - fmt.Sprintf("ln -s %s/all-machines.log %s/", mcfg.LogDir, env.config.logDir()), - fmt.Sprintf("ln -s %s/machine-0.log %s/", env.config.logDir(), mcfg.LogDir)) + ) if err := cloudinit.ConfigureJuju(mcfg, cloudcfg); err != nil { return err } @@ -404,7 +407,7 @@ return err } args := []string{ - osenv.JujuHomeEnvKey + "=" + osenv.JujuHome(), + "env", osenv.JujuHomeEnvKey + "=" + osenv.JujuHome(), juju, "destroy-environment", "-y", "--force", env.Name(), } cmd := exec.Command("sudo", args...) @@ -437,6 +440,12 @@ } } } + // Stop the mongo database and machine agent. It's possible that the + // service doesn't exist or is not running, so don't check the error. + upstart.NewService(env.mongoServiceName()).StopAndRemove() + upstart.NewService(env.machineAgentServiceName()).StopAndRemove() + + // Finally, remove the data-dir. if err := os.RemoveAll(env.config.rootDir()); err != nil && !os.IsNotExist(err) { return err } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/environ_test.go juju-core-1.17.7/src/launchpad.net/juju-core/provider/local/environ_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/environ_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/provider/local/environ_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -4,6 +4,7 @@ package local_test import ( + "fmt" "io/ioutil" "os" "path/filepath" @@ -14,6 +15,9 @@ coreCloudinit "launchpad.net/juju-core/cloudinit" "launchpad.net/juju-core/constraints" + "launchpad.net/juju-core/container" + "launchpad.net/juju-core/container/lxc" + containertesting "launchpad.net/juju-core/container/testing" "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/cloudinit" "launchpad.net/juju-core/environs/config" @@ -25,6 +29,7 @@ "launchpad.net/juju-core/provider/local" "launchpad.net/juju-core/state/api/params" coretesting "launchpad.net/juju-core/testing" + "launchpad.net/juju-core/upstart" ) const echoCommandScript = "#!/bin/sh\necho $0 \"$@\" >> $0.args" @@ -93,7 +98,6 @@ type localJujuTestSuite struct { baseProviderSuite jujutest.Tests - restoreRootCheck func() oldUpstartLocation string testPath string dbServiceName string @@ -115,17 +119,20 @@ // Add in an admin secret s.Tests.TestConfig["admin-secret"] = "sekrit" - s.restoreRootCheck = local.SetRootCheckFunction(func() bool { return false }) + s.PatchValue(local.CheckIfRoot, func() bool { return false }) s.Tests.SetUpTest(c) cfg, err := config.New(config.NoDefaults, s.TestConfig) c.Assert(err, gc.IsNil) s.dbServiceName = "juju-db-" + local.ConfigNamespace(cfg) + + s.PatchValue(local.FinishBootstrap, func(mcfg *cloudinit.MachineConfig, cloudcfg *coreCloudinit.Config, ctx environs.BootstrapContext) error { + return nil + }) } func (s *localJujuTestSuite) TearDownTest(c *gc.C) { s.Tests.TearDownTest(c) - s.restoreRootCheck() s.baseProviderSuite.TearDownTest(c) } @@ -154,10 +161,9 @@ c.Skip("StartInstance not implemented yet.") } -func (s *localJujuTestSuite) testBootstrap(c *gc.C) (env environs.Environ) { - testConfig := minimalConfig(c) +func (s *localJujuTestSuite) testBootstrap(c *gc.C, cfg *config.Config) (env environs.Environ) { ctx := coretesting.Context(c) - environ, err := local.Provider.Prepare(ctx, testConfig) + environ, err := local.Provider.Prepare(ctx, cfg) c.Assert(err, gc.IsNil) envtesting.UploadFakeTools(c, environ.Storage()) defer environ.Storage().RemoveAll() @@ -176,14 +182,11 @@ c.Assert(mcfg.Jobs, gc.DeepEquals, []params.MachineJob{params.JobManageEnviron}) return nil }) - s.testBootstrap(c) + s.testBootstrap(c, minimalConfig(c)) } func (s *localJujuTestSuite) TestDestroy(c *gc.C) { - s.PatchValue(local.FinishBootstrap, func(mcfg *cloudinit.MachineConfig, cloudcfg *coreCloudinit.Config, ctx environs.BootstrapContext) error { - return nil - }) - env := s.testBootstrap(c) + env := s.testBootstrap(c, minimalConfig(c)) err := env.Destroy() // Succeeds because there's no "agents" directory, // so destroy will just return without attempting @@ -192,21 +195,23 @@ c.Assert(s.fakesudo+".args", jc.DoesNotExist) } -func (s *localJujuTestSuite) TestDestroyCallSudo(c *gc.C) { - s.PatchValue(local.FinishBootstrap, func(mcfg *cloudinit.MachineConfig, cloudcfg *coreCloudinit.Config, ctx environs.BootstrapContext) error { - return nil - }) - env := s.testBootstrap(c) +func (s *localJujuTestSuite) makeAgentsDir(c *gc.C, env environs.Environ) { rootDir := env.Config().AllAttrs()["root-dir"].(string) agentsDir := filepath.Join(rootDir, "agents") err := os.Mkdir(agentsDir, 0755) c.Assert(err, gc.IsNil) - err = env.Destroy() +} + +func (s *localJujuTestSuite) TestDestroyCallSudo(c *gc.C) { + env := s.testBootstrap(c, minimalConfig(c)) + s.makeAgentsDir(c, env) + err := env.Destroy() c.Assert(err, gc.IsNil) data, err := ioutil.ReadFile(s.fakesudo + ".args") c.Assert(err, gc.IsNil) expected := []string{ s.fakesudo, + "env", "JUJU_HOME=" + osenv.JujuHome(), os.Args[0], "destroy-environment", @@ -216,3 +221,88 @@ } c.Assert(string(data), gc.Equals, strings.Join(expected, " ")+"\n") } + +func (s *localJujuTestSuite) makeFakeUpstartScripts(c *gc.C, env environs.Environ, +) (mongo *upstart.Service, machineAgent *upstart.Service) { + upstartDir := c.MkDir() + s.PatchValue(&upstart.InitDir, upstartDir) + s.MakeTool(c, "start", `echo "some-service start/running, process 123"`) + + namespace := env.Config().AllAttrs()["namespace"].(string) + mongo = upstart.NewService(fmt.Sprintf("juju-db-%s", namespace)) + mongoConf := upstart.Conf{ + Service: *mongo, + Desc: "fake mongo", + Cmd: "echo FAKE", + } + err := mongoConf.Install() + c.Assert(err, gc.IsNil) + c.Assert(mongo.Installed(), jc.IsTrue) + + machineAgent = upstart.NewService(fmt.Sprintf("juju-agent-%s", namespace)) + agentConf := upstart.Conf{ + Service: *machineAgent, + Desc: "fake agent", + Cmd: "echo FAKE", + } + err = agentConf.Install() + c.Assert(err, gc.IsNil) + c.Assert(machineAgent.Installed(), jc.IsTrue) + + return mongo, machineAgent +} + +func (s *localJujuTestSuite) TestDestroyRemovesUpstartServices(c *gc.C) { + env := s.testBootstrap(c, minimalConfig(c)) + s.makeAgentsDir(c, env) + mongo, machineAgent := s.makeFakeUpstartScripts(c, env) + s.PatchValue(local.CheckIfRoot, func() bool { return true }) + + err := env.Destroy() + c.Assert(err, gc.IsNil) + + c.Assert(mongo.Installed(), jc.IsFalse) + c.Assert(machineAgent.Installed(), jc.IsFalse) +} + +func (s *localJujuTestSuite) TestDestroyRemovesContainers(c *gc.C) { + env := s.testBootstrap(c, minimalConfig(c)) + s.makeAgentsDir(c, env) + s.PatchValue(local.CheckIfRoot, func() bool { return true }) + + namespace := env.Config().AllAttrs()["namespace"].(string) + manager, err := lxc.NewContainerManager(container.ManagerConfig{ + container.ConfigName: namespace, + container.ConfigLogDir: "logdir", + }) + c.Assert(err, gc.IsNil) + + machine1 := containertesting.CreateContainer(c, manager, "1") + + err = env.Destroy() + c.Assert(err, gc.IsNil) + + container := s.Factory.New(string(machine1.Id())) + c.Assert(container.IsConstructed(), jc.IsFalse) +} + +func (s *localJujuTestSuite) TestBootstrapRemoveLeftovers(c *gc.C) { + cfg := minimalConfig(c) + rootDir := cfg.AllAttrs()["root-dir"].(string) + + // Create a dir inside local/log that should be removed by Bootstrap. + logThings := filepath.Join(rootDir, "log", "things") + err := os.MkdirAll(logThings, 0755) + c.Assert(err, gc.IsNil) + + // Create a cloud-init-output.log in root-dir that should be + // removed/truncated by Bootstrap. + cloudInitOutputLog := filepath.Join(rootDir, "cloud-init-output.log") + err = ioutil.WriteFile(cloudInitOutputLog, []byte("ohai"), 0644) + c.Assert(err, gc.IsNil) + + s.testBootstrap(c, cfg) + c.Assert(logThings, jc.DoesNotExist) + c.Assert(cloudInitOutputLog, jc.DoesNotExist) + c.Assert(filepath.Join(rootDir, "log"), jc.IsSymlink) +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/export_test.go juju-core-1.17.7/src/launchpad.net/juju-core/provider/local/export_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/provider/local/export_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/provider/local/export_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -10,6 +10,7 @@ ) var ( + CheckIfRoot = &checkIfRoot CheckLocalPort = &checkLocalPort DetectAptProxies = &detectAptProxies FinishBootstrap = &finishBootstrap @@ -19,14 +20,6 @@ UserCurrent = &userCurrent ) -// SetRootCheckFunction allows tests to override the check for a root user. -// The return value is the function to restore the old value. -func SetRootCheckFunction(f func() bool) func() { - old := checkIfRoot - checkIfRoot = f - return func() { checkIfRoot = old } -} - // ConfigNamespace returns the result of the namespace call on the // localConfig. func ConfigNamespace(cfg *config.Config) string { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/provider/maas/environ.go juju-core-1.17.7/src/launchpad.net/juju-core/provider/maas/environ.go --- juju-core-1.17.6/src/launchpad.net/juju-core/provider/maas/environ.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/provider/maas/environ.go 2014-03-27 15:48:42.000000000 +0000 @@ -14,6 +14,7 @@ "launchpad.net/gomaasapi" "launchpad.net/juju-core/agent" + "launchpad.net/juju-core/cloudinit" "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/config" @@ -193,13 +194,13 @@ // url.Values object suitable to pass to MAAS when acquiring a node. func addNetworks(params url.Values, nets environs.Networks) { // Network Inclusion/Exclusion setup - if nets.IncludedNetworks != nil { - for _, network_name := range nets.IncludedNetworks { + if nets.IncludeNetworks != nil { + for _, network_name := range nets.IncludeNetworks { params.Add("networks", network_name) } } - if nets.ExcludedNetworks != nil { - for _, not_network_name := range nets.ExcludedNetworks { + if nets.ExcludeNetworks != nil { + for _, not_network_name := range nets.ExcludeNetworks { params.Add("not_networks", not_network_name) } } @@ -291,10 +292,6 @@ if err != nil { return nil, nil, err } - additionalScripts, err := additionalScripts(hostname) - if err != nil { - return nil, nil, err - } if err := environs.FinishMachineConfig(args.MachineConfig, environ.Config(), args.Constraints); err != nil { return nil, nil, err } @@ -302,7 +299,11 @@ // The machine envronment config values are being moved to the agent config. // Explicitly specify that the lxc containers use the network bridge defined above. args.MachineConfig.AgentEnvironment[agent.LxcBridge] = "br0" - userdata, err := environs.ComposeUserData(args.MachineConfig, additionalScripts...) + cloudcfg, err := newCloudinitConfig(hostname) + if err != nil { + return nil, nil, err + } + userdata, err := environs.ComposeUserData(args.MachineConfig, cloudcfg) if err != nil { msg := fmt.Errorf("could not compose userdata for bootstrap node: %v", err) return nil, nil, msg @@ -318,23 +319,26 @@ return inst, nil, nil } -// additionalScripts is an additional set of commands -// to run during cloud-init (before the synchronous phase). -func additionalScripts(hostname string) ([]string, error) { +// newCloudinitConfig creates a cloudinit.Config structure +// suitable as a base for initialising a MAAS node. +func newCloudinitConfig(hostname string) (*cloudinit.Config, error) { info := machineInfo{hostname} runCmd, err := info.cloudinitRunCmd() if err != nil { return nil, err } - return []string{ + cloudcfg := cloudinit.New() + cloudcfg.SetAptUpdate(true) + cloudcfg.AddPackage("bridge-utils") + cloudcfg.AddScripts( + "set -xe", runCmd, - utils.CommandString(utils.AptGetCommand("update")...), - utils.CommandString(utils.AptGetCommand("install", "bridge-utils")...), "ifdown eth0", createBridgeNetwork(), linkBridgeInInterfaces(), "ifup br0", - }, nil + ) + return cloudcfg, nil } // StartInstance is specified in the InstanceBroker interface. diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/provider/maas/environ_test.go juju-core-1.17.7/src/launchpad.net/juju-core/provider/maas/environ_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/provider/maas/environ_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/provider/maas/environ_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -6,6 +6,7 @@ import ( stdtesting "testing" + jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/gomaasapi" @@ -194,14 +195,13 @@ c.Check(env.Name(), gc.Equals, "testenv") } -func (*environSuite) TestAdditionalSCripts(c *gc.C) { - const aptGetPrefix = "env DEBIAN_FRONTEND=noninteractive apt-get --option=Dpkg::Options::=--force-confold --option=Dpkg::options::=--force-unsafe-io --assume-yes --quiet" - scripts, err := maas.AdditionalScripts("testing.invalid") +func (*environSuite) TestNewCloudinitConfig(c *gc.C) { + cloudcfg, err := maas.NewCloudinitConfig("testing.invalid") c.Assert(err, gc.IsNil) - c.Assert(scripts, gc.DeepEquals, []string{ + c.Assert(cloudcfg.AptUpdate(), jc.IsTrue) + c.Assert(cloudcfg.RunCmds(), gc.DeepEquals, []interface{}{ + "set -xe", "mkdir -p '/var/lib/juju'; echo -n 'hostname: testing.invalid\n' > '/var/lib/juju/MAASmachine.txt'", - aptGetPrefix + " update", - aptGetPrefix + " install bridge-utils", "ifdown eth0", "cat > /etc/network/eth0.config << EOF\niface eth0 inet manual\n\nauto br0\niface br0 inet dhcp\n bridge_ports eth0\nEOF\n", `sed -i "s/iface eth0 inet dhcp/source \/etc\/network\/eth0.config/" /etc/network/interfaces`, diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/provider/maas/environ_whitebox_test.go juju-core-1.17.7/src/launchpad.net/juju-core/provider/maas/environ_whitebox_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/provider/maas/environ_whitebox_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/provider/maas/environ_whitebox_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -300,20 +300,20 @@ }, { environs.Networks{ - IncludedNetworks: []string{"included_net_1"}, + IncludeNetworks: []string{"included_net_1"}, }, url.Values{"networks": {"included_net_1"}}, }, { environs.Networks{ - ExcludedNetworks: []string{"excluded_net_1"}, + ExcludeNetworks: []string{"excluded_net_1"}, }, url.Values{"not_networks": {"excluded_net_1"}}, }, { environs.Networks{ - IncludedNetworks: []string{"included_net_1", "included_net_2"}, - ExcludedNetworks: []string{"excluded_net_1", "excluded_net_2"}, + IncludeNetworks: []string{"included_net_1", "included_net_2"}, + ExcludeNetworks: []string{"excluded_net_1", "excluded_net_2"}, }, url.Values{ "networks": {"included_net_1", "included_net_2"}, diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/provider/maas/export_test.go juju-core-1.17.7/src/launchpad.net/juju-core/provider/maas/export_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/provider/maas/export_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/provider/maas/export_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -10,9 +10,9 @@ ) var ( - ShortAttempt = &shortAttempt - APIVersion = apiVersion - AdditionalScripts = additionalScripts + ShortAttempt = &shortAttempt + APIVersion = apiVersion + NewCloudinitConfig = newCloudinitConfig ) func MAASAgentName(env environs.Environ) string { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/provider/manual/environ.go juju-core-1.17.7/src/launchpad.net/juju-core/provider/manual/environ.go --- juju-core-1.17.6/src/launchpad.net/juju-core/provider/manual/environ.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/provider/manual/environ.go 2014-03-27 15:48:42.000000000 +0000 @@ -25,9 +25,11 @@ "launchpad.net/juju-core/environs/storage" envtools "launchpad.net/juju-core/environs/tools" "launchpad.net/juju-core/instance" + "launchpad.net/juju-core/juju/arch" "launchpad.net/juju-core/provider/common" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" + "launchpad.net/juju-core/utils" "launchpad.net/juju-core/utils/ssh" "launchpad.net/juju-core/worker/localstorage" "launchpad.net/juju-core/worker/terminationworker" @@ -88,13 +90,7 @@ // SupportedArchitectures is specified on the EnvironCapability interface. func (e *manualEnviron) SupportedArchitectures() ([]string, error) { - envConfig := e.envConfig() - host := envConfig.bootstrapHost() - hc, _, err := manual.DetectSeriesAndHardwareCharacteristics(host) - if err != nil { - return nil, err - } - return []string{*hc.Arch}, nil + return arch.AllSupportedArches, nil } func (e *manualEnviron) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error { @@ -112,7 +108,7 @@ if err != nil { return err } - selectedTools, err := common.EnsureBootstrapTools(e, series, hc.Arch) + selectedTools, err := common.EnsureBootstrapTools(ctx, e, series, hc.Arch) if err != nil { return err } @@ -218,18 +214,34 @@ return e.storage } -var runSSHCommand = func(host string, command []string) (stderr string, err error) { +var runSSHCommand = func(host string, command []string, stdin string) (stderr string, err error) { cmd := ssh.Command(host, command, nil) var stderrBuf bytes.Buffer + cmd.Stdin = strings.NewReader(stdin) cmd.Stderr = &stderrBuf err = cmd.Run() return stderrBuf.String(), err } func (e *manualEnviron) Destroy() error { + script := ` +set -x +pkill -%d jujud && exit +stop juju-db +rm -f /etc/init/juju* +rm -f /etc/rsyslog.d/*juju* +rm -fr %s %s +exit 0 +` + script = fmt.Sprintf( + script, + terminationworker.TerminationSignal, + utils.ShQuote(agent.DefaultDataDir), + utils.ShQuote(agent.DefaultLogDir), + ) stderr, err := runSSHCommand( "ubuntu@"+e.envConfig().bootstrapHost(), - []string{"sudo", "pkill", fmt.Sprintf("-%d", terminationworker.TerminationSignal), "jujud"}, + []string{"sudo", "/bin/bash"}, script, ) if err != nil { if stderr := strings.TrimSpace(stderr); len(stderr) > 0 { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/provider/manual/environ_test.go juju-core-1.17.7/src/launchpad.net/juju-core/provider/manual/environ_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/provider/manual/environ_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/provider/manual/environ_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -15,6 +15,7 @@ "launchpad.net/juju-core/environs/storage" "launchpad.net/juju-core/environs/tools" "launchpad.net/juju-core/instance" + "launchpad.net/juju-core/juju/arch" "launchpad.net/juju-core/testing/testbase" ) @@ -84,9 +85,18 @@ func (s *environSuite) TestDestroy(c *gc.C) { var resultStderr string var resultErr error - runSSHCommandTesting := func(host string, command []string) (string, error) { + runSSHCommandTesting := func(host string, command []string, stdin string) (string, error) { c.Assert(host, gc.Equals, "ubuntu@hostname") - c.Assert(command, gc.DeepEquals, []string{"sudo", "pkill", "-6", "jujud"}) + c.Assert(command, gc.DeepEquals, []string{"sudo", "/bin/bash"}) + c.Assert(stdin, gc.DeepEquals, ` +set -x +pkill -6 jujud && exit +stop juju-db +rm -f /etc/init/juju* +rm -f /etc/rsyslog.d/*juju* +rm -fr '/var/lib/juju' '/var/log/juju' +exit 0 +`) return resultStderr, resultErr } s.PatchValue(&runSSHCommand, runSSHCommandTesting) @@ -131,11 +141,7 @@ } func (s *environSuite) TestSupportedArchitectures(c *gc.C) { - s.PatchValue(&manual.DetectSeriesAndHardwareCharacteristics, func(host string) (instance.HardwareCharacteristics, string, error) { - c.Assert(host, gc.Equals, "hostname") - return instance.MustParseHardware("arch=arm64"), "precise", nil - }) - a, err := s.env.SupportedArchitectures() + arches, err := s.env.SupportedArchitectures() c.Assert(err, gc.IsNil) - c.Assert(a, gc.DeepEquals, []string{"arm64"}) + c.Assert(arches, gc.DeepEquals, arch.AllSupportedArches) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/provider/openstack/provider.go juju-core-1.17.7/src/launchpad.net/juju-core/provider/openstack/provider.go --- juju-core-1.17.6/src/launchpad.net/juju-core/provider/openstack/provider.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/provider/openstack/provider.go 2014-03-27 15:48:42.000000000 +0000 @@ -645,9 +645,9 @@ // Add the simplestreams base URL from keystone if it is defined. productStreamsURL, err := e.client.MakeServiceURL("product-streams", nil) if err == nil { - verify := simplestreams.VerifySSLHostnames + verify := utils.VerifySSLHostnames if !e.Config().SSLHostnameVerification() { - verify = simplestreams.NoVerifySSLHostnames + verify = utils.NoVerifySSLHostnames } source := simplestreams.NewURLDataSource("keystone catalog", productStreamsURL, verify) e.imageSources = append(e.imageSources, source) @@ -669,9 +669,9 @@ return nil, err } } - verify := simplestreams.VerifySSLHostnames + verify := utils.VerifySSLHostnames if !e.Config().SSLHostnameVerification() { - verify = simplestreams.NoVerifySSLHostnames + verify = utils.NoVerifySSLHostnames } // Add the simplestreams source off the control bucket. e.toolsSources = append(e.toolsSources, storage.NewStorageSimpleStreamsDataSource( @@ -790,7 +790,7 @@ if err := environs.FinishMachineConfig(args.MachineConfig, e.Config(), args.Constraints); err != nil { return nil, nil, err } - userData, err := environs.ComposeUserData(args.MachineConfig) + userData, err := environs.ComposeUserData(args.MachineConfig, nil) if err != nil { return nil, nil, fmt.Errorf("cannot make user data: %v", err) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/scripts/win-installer/setup.iss juju-core-1.17.7/src/launchpad.net/juju-core/scripts/win-installer/setup.iss --- juju-core-1.17.6/src/launchpad.net/juju-core/scripts/win-installer/setup.iss 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/scripts/win-installer/setup.iss 2014-03-27 15:48:42.000000000 +0000 @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Juju" -#define MyAppVersion "1.17.6" +#define MyAppVersion "1.17.7" #define MyAppPublisher "Canonical, Ltd" #define MyAppURL "http://juju.ubuntu.com/" #define MyAppExeName "juju.exe" diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/addmachine.go juju-core-1.17.7/src/launchpad.net/juju-core/state/addmachine.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/addmachine.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/addmachine.go 2014-03-27 15:48:42.000000000 +0000 @@ -53,6 +53,14 @@ // be associated with the machine. HardwareCharacteristics instance.HardwareCharacteristics + // IncludeNetworks holds a list of networks the machine should be + // part of. + IncludeNetworks []string + + // ExcludeNetworks holds a list of network the machine should not + // be part of. + ExcludeNetworks []string + // Nonce holds a unique value that can be used to check // if a new instance was really started for this machine. // See Machine.SetProvisioned. This must be set if InstanceId is set. @@ -230,7 +238,7 @@ } mdoc := machineDocForTemplate(template, strconv.Itoa(seq)) var ops []txn.Op - ops = append(ops, st.insertNewMachineOps(mdoc, template.Constraints)...) + ops = append(ops, st.insertNewMachineOps(mdoc, template)...) ops = append(ops, st.insertNewContainerRefOp(mdoc.Id)) if template.InstanceId != "" { ops = append(ops, txn.Op{ @@ -301,7 +309,7 @@ mdoc := machineDocForTemplate(template, newId) mdoc.ContainerType = string(containerType) var ops []txn.Op - ops = append(ops, st.insertNewMachineOps(mdoc, template.Constraints)...) + ops = append(ops, st.insertNewMachineOps(mdoc, template)...) ops = append(ops, // Update containers record for host machine. st.addChildToContainerRefOp(parentId, mdoc.Id), @@ -358,8 +366,8 @@ mdoc := machineDocForTemplate(template, newId) mdoc.ContainerType = string(containerType) var ops []txn.Op - ops = append(ops, st.insertNewMachineOps(parentDoc, parentTemplate.Constraints)...) - ops = append(ops, st.insertNewMachineOps(mdoc, template.Constraints)...) + ops = append(ops, st.insertNewMachineOps(parentDoc, parentTemplate)...) + ops = append(ops, st.insertNewMachineOps(mdoc, template)...) ops = append(ops, // The host machine doesn't exist yet, create a new containers record. st.insertNewContainerRefOp(mdoc.Id), @@ -385,8 +393,9 @@ } // insertNewMachineOps returns operations to insert the given machine -// document and its associated constraints into the database. -func (st *State) insertNewMachineOps(mdoc *machineDoc, cons constraints.Value) []txn.Op { +// document into the database, based on the given template. Only the +// constraints and networks are used from the template. +func (st *State) insertNewMachineOps(mdoc *machineDoc, template MachineTemplate) []txn.Op { return []txn.Op{ { C: st.machines.Name, @@ -394,10 +403,14 @@ Assert: txn.DocMissing, Insert: mdoc, }, - createConstraintsOp(st, machineGlobalKey(mdoc.Id), cons), + createConstraintsOp(st, machineGlobalKey(mdoc.Id), template.Constraints), createStatusOp(st, machineGlobalKey(mdoc.Id), statusDoc{ Status: params.StatusPending, }), + createNetworksOp(st, machineGlobalKey(mdoc.Id), + template.IncludeNetworks, + template.ExcludeNetworks, + ), } } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/client.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/client.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/client.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/client.go 2014-03-27 15:48:42.000000000 +0000 @@ -26,6 +26,17 @@ st *State } +// NetworksSpecification holds the enabled and disabled networks for a +// service. +type NetworksSpecification struct { + Enabled []string + Disabled []string +} + +func (c *Client) call(method string, params, result interface{}) error { + return c.st.Call("Client", "", method, params, result) +} + // MachineStatus holds status info about a machine. type MachineStatus struct { Err error @@ -49,6 +60,7 @@ Exposed bool Life string Relations map[string][]string + Networks NetworksSpecification CanUpgradeTo string SubordinateTo []string Units map[string]UnitStatus @@ -79,7 +91,7 @@ func (c *Client) Status(patterns []string) (*Status, error) { var result Status p := params.StatusParams{Patterns: patterns} - if err := c.st.Call("Client", "", "FullStatus", p, &result); err != nil { + if err := c.call("FullStatus", p, &result); err != nil { return nil, err } return &result, nil @@ -99,7 +111,7 @@ // removed along with structs when api versioning makes it safe to do so. func (c *Client) LegacyStatus() (*LegacyStatus, error) { var result LegacyStatus - if err := c.st.Call("Client", "", "Status", nil, &result); err != nil { + if err := c.call("Status", nil, &result); err != nil { return nil, err } return &result, nil @@ -113,7 +125,7 @@ } // TODO(Nate): Put this back to ServiceSet when the GUI stops expecting // ServiceSet to unset values set to an empty string. - return c.st.Call("Client", "", "NewServiceSetForClientAPI", p, nil) + return c.call("NewServiceSetForClientAPI", p, nil) } // ServiceUnset resets configuration options on a service. @@ -122,7 +134,7 @@ ServiceName: service, Options: options, } - return c.st.Call("Client", "", "ServiceUnset", p, nil) + return c.call("ServiceUnset", p, nil) } // Resolved clears errors on a unit. @@ -131,7 +143,20 @@ UnitName: unit, Retry: retry, } - return c.st.Call("Client", "", "Resolved", p, nil) + return c.call("Resolved", p, nil) +} + +// RetryProvisioning updates the provisioning status of a machine allowing the +// provisioner to retry. +func (c *Client) RetryProvisioning(machines ...string) ([]params.ErrorResult, error) { + p := params.Entities{} + p.Entities = make([]params.Entity, len(machines)) + for i, machine := range machines { + p.Entities[i] = params.Entity{Tag: machine} + } + var results params.ErrorResults + err := c.st.Call("Client", "", "RetryProvisioning", p, &results) + return results.Results, err } // PublicAddress returns the public address of the specified @@ -139,7 +164,7 @@ func (c *Client) PublicAddress(target string) (string, error) { var results params.PublicAddressResults p := params.PublicAddress{Target: target} - err := c.st.Call("Client", "", "PublicAddress", p, &results) + err := c.call("PublicAddress", p, &results) return results.PublicAddress, err } @@ -148,7 +173,7 @@ func (c *Client) PrivateAddress(target string) (string, error) { var results params.PrivateAddressResults p := params.PrivateAddress{Target: target} - err := c.st.Call("Client", "", "PrivateAddress", p, &results) + err := c.call("PrivateAddress", p, &results) return results.PrivateAddress, err } @@ -159,14 +184,14 @@ ServiceName: service, Config: yaml, } - return c.st.Call("Client", "", "ServiceSetYAML", p, nil) + return c.call("ServiceSetYAML", p, nil) } // ServiceGet returns the configuration for the named service. func (c *Client) ServiceGet(service string) (*params.ServiceGetResults, error) { var results params.ServiceGetResults params := params.ServiceGet{ServiceName: service} - err := c.st.Call("Client", "", "ServiceGet", params, &results) + err := c.call("ServiceGet", params, &results) return &results, err } @@ -174,21 +199,21 @@ func (c *Client) AddRelation(endpoints ...string) (*params.AddRelationResults, error) { var addRelRes params.AddRelationResults params := params.AddRelation{Endpoints: endpoints} - err := c.st.Call("Client", "", "AddRelation", params, &addRelRes) + err := c.call("AddRelation", params, &addRelRes) return &addRelRes, err } // DestroyRelation removes the relation between the specified endpoints. func (c *Client) DestroyRelation(endpoints ...string) error { params := params.DestroyRelation{Endpoints: endpoints} - return c.st.Call("Client", "", "DestroyRelation", params, nil) + return c.call("DestroyRelation", params, nil) } // ServiceCharmRelations returns the service's charms relation names. func (c *Client) ServiceCharmRelations(service string) ([]string, error) { var results params.ServiceCharmRelationsResults params := params.ServiceCharmRelations{ServiceName: service} - err := c.st.Call("Client", "", "ServiceCharmRelations", params, &results) + err := c.call("ServiceCharmRelations", params, &results) return results.CharmRelations, err } @@ -198,7 +223,7 @@ MachineParams: machineParams, } results := new(params.AddMachinesResults) - err := c.st.Call("Client", "", "AddMachines", args, results) + err := c.call("AddMachines", args, results) return results.Machines, err } @@ -206,7 +231,7 @@ // provisions a machine agent on the machine executing the script. func (c *Client) ProvisioningScript(args params.ProvisioningScriptParams) (script string, err error) { var result params.ProvisioningScriptResult - if err = c.st.Call("Client", "", "ProvisioningScript", args, &result); err != nil { + if err = c.call("ProvisioningScript", args, &result); err != nil { return "", err } return result.Script, nil @@ -215,48 +240,65 @@ // DestroyMachines removes a given set of machines. func (c *Client) DestroyMachines(machines ...string) error { params := params.DestroyMachines{MachineNames: machines} - return c.st.Call("Client", "", "DestroyMachines", params, nil) + return c.call("DestroyMachines", params, nil) } // ForceDestroyMachines removes a given set of machines and all associated units. func (c *Client) ForceDestroyMachines(machines ...string) error { params := params.DestroyMachines{Force: true, MachineNames: machines} - return c.st.Call("Client", "", "DestroyMachines", params, nil) + return c.call("DestroyMachines", params, nil) } // ServiceExpose changes the juju-managed firewall to expose any ports that // were also explicitly marked by units as open. func (c *Client) ServiceExpose(service string) error { params := params.ServiceExpose{ServiceName: service} - return c.st.Call("Client", "", "ServiceExpose", params, nil) + return c.call("ServiceExpose", params, nil) } // ServiceUnexpose changes the juju-managed firewall to unexpose any ports that // were also explicitly marked by units as open. func (c *Client) ServiceUnexpose(service string) error { params := params.ServiceUnexpose{ServiceName: service} - return c.st.Call("Client", "", "ServiceUnexpose", params, nil) + return c.call("ServiceUnexpose", params, nil) +} + +// ServiceDeployWithNetworks works exactly like ServiceDeploy, but +// allows specifying networks to either include or exclude on the +// machine where the charm is deployed. +func (c *Client) ServiceDeployWithNetworks(charmURL string, serviceName string, numUnits int, configYAML string, cons constraints.Value, toMachineSpec string, includeNetworks, excludeNetworks []string) error { + params := params.ServiceDeploy{ + ServiceName: serviceName, + CharmUrl: charmURL, + NumUnits: numUnits, + ConfigYAML: configYAML, + Constraints: cons, + ToMachineSpec: toMachineSpec, + IncludeNetworks: includeNetworks, + ExcludeNetworks: excludeNetworks, + } + return c.st.Call("Client", "", "ServiceDeployWithNetworks", params, nil) } // ServiceDeploy obtains the charm, either locally or from the charm store, // and deploys it. -func (c *Client) ServiceDeploy(charmUrl string, serviceName string, numUnits int, configYAML string, cons constraints.Value, toMachineSpec string) error { +func (c *Client) ServiceDeploy(charmURL string, serviceName string, numUnits int, configYAML string, cons constraints.Value, toMachineSpec string) error { params := params.ServiceDeploy{ ServiceName: serviceName, - CharmUrl: charmUrl, + CharmUrl: charmURL, NumUnits: numUnits, ConfigYAML: configYAML, Constraints: cons, ToMachineSpec: toMachineSpec, } - return c.st.Call("Client", "", "ServiceDeploy", params, nil) + return c.call("ServiceDeploy", params, nil) } // ServiceUpdate updates the service attributes, including charm URL, // minimum number of units, settings and constraints. // TODO(frankban) deprecate redundant API calls that this supercedes. func (c *Client) ServiceUpdate(args params.ServiceUpdate) error { - return c.st.Call("Client", "", "ServiceUpdate", args, nil) + return c.call("ServiceUpdate", args, nil) } // ServiceSetCharm sets the charm for a given service. @@ -266,7 +308,7 @@ CharmUrl: charmUrl, Force: force, } - return c.st.Call("Client", "", "ServiceSetCharm", args, nil) + return c.call("ServiceSetCharm", args, nil) } // ServiceGetCharmURL returns the charm URL the given service is @@ -274,7 +316,7 @@ func (c *Client) ServiceGetCharmURL(serviceName string) (*charm.URL, error) { result := new(params.StringResult) args := params.ServiceGet{ServiceName: serviceName} - err := c.st.Call("Client", "", "ServiceGetCharmURL", args, &result) + err := c.call("ServiceGetCharmURL", args, &result) if err != nil { return nil, err } @@ -289,14 +331,14 @@ ToMachineSpec: machineSpec, } results := new(params.AddServiceUnitsResults) - err := c.st.Call("Client", "", "AddServiceUnits", args, results) + err := c.call("AddServiceUnits", args, results) return results.Units, err } // DestroyServiceUnits decreases the number of units dedicated to a service. func (c *Client) DestroyServiceUnits(unitNames ...string) error { params := params.DestroyServiceUnits{unitNames} - return c.st.Call("Client", "", "DestroyServiceUnits", params, nil) + return c.call("DestroyServiceUnits", params, nil) } // ServiceDestroy destroys a given service. @@ -304,20 +346,20 @@ params := params.ServiceDestroy{ ServiceName: service, } - return c.st.Call("Client", "", "ServiceDestroy", params, nil) + return c.call("ServiceDestroy", params, nil) } // GetServiceConstraints returns the constraints for the given service. func (c *Client) GetServiceConstraints(service string) (constraints.Value, error) { results := new(params.GetConstraintsResults) - err := c.st.Call("Client", "", "GetServiceConstraints", params.GetServiceConstraints{service}, results) + err := c.call("GetServiceConstraints", params.GetServiceConstraints{service}, results) return results.Constraints, err } // GetEnvironmentConstraints returns the constraints for the environment. func (c *Client) GetEnvironmentConstraints() (constraints.Value, error) { results := new(params.GetConstraintsResults) - err := c.st.Call("Client", "", "GetEnvironmentConstraints", nil, results) + err := c.call("GetEnvironmentConstraints", nil, results) return results.Constraints, err } @@ -327,7 +369,7 @@ ServiceName: service, Constraints: constraints, } - return c.st.Call("Client", "", "SetServiceConstraints", params, nil) + return c.call("SetServiceConstraints", params, nil) } // SetEnvironmentConstraints specifies the constraints for the environment. @@ -335,7 +377,7 @@ params := params.SetConstraints{ Constraints: constraints, } - return c.st.Call("Client", "", "SetEnvironmentConstraints", params, nil) + return c.call("SetEnvironmentConstraints", params, nil) } // CharmInfo holds information about a charm. @@ -350,7 +392,7 @@ func (c *Client) CharmInfo(charmURL string) (*CharmInfo, error) { args := params.CharmInfo{CharmURL: charmURL} info := new(CharmInfo) - if err := c.st.Call("Client", "", "CharmInfo", args, info); err != nil { + if err := c.call("CharmInfo", args, info); err != nil { return nil, err } return info, nil @@ -367,7 +409,7 @@ // EnvironmentInfo returns details about the Juju environment. func (c *Client) EnvironmentInfo() (*EnvironmentInfo, error) { info := new(EnvironmentInfo) - err := c.st.Call("Client", "", "EnvironmentInfo", nil, info) + err := c.call("EnvironmentInfo", nil, info) return info, err } @@ -380,7 +422,7 @@ // collection of Deltas. func (c *Client) WatchAll() (*AllWatcher, error) { info := new(WatchAll) - if err := c.st.Call("Client", "", "WatchAll", nil, info); err != nil { + if err := c.call("WatchAll", nil, info); err != nil { return nil, err } return newAllWatcher(c, &info.AllWatcherId), nil @@ -390,7 +432,7 @@ func (c *Client) GetAnnotations(tag string) (map[string]string, error) { args := params.GetAnnotations{tag} ann := new(params.GetAnnotationsResults) - err := c.st.Call("Client", "", "GetAnnotations", args, ann) + err := c.call("GetAnnotations", args, ann) return ann.Annotations, err } @@ -399,7 +441,7 @@ // units and the environment itself. func (c *Client) SetAnnotations(tag string, pairs map[string]string) error { args := params.SetAnnotations{tag, pairs} - return c.st.Call("Client", "", "SetAnnotations", args, nil) + return c.call("SetAnnotations", args, nil) } // Close closes the Client's underlying State connection @@ -413,21 +455,27 @@ // EnvironmentGet returns all environment settings. func (c *Client) EnvironmentGet() (map[string]interface{}, error) { result := params.EnvironmentGetResults{} - err := c.st.Call("Client", "", "EnvironmentGet", nil, &result) + err := c.call("EnvironmentGet", nil, &result) return result.Config, err } // EnvironmentSet sets the given key-value pairs in the environment. func (c *Client) EnvironmentSet(config map[string]interface{}) error { args := params.EnvironmentSet{Config: config} - return c.st.Call("Client", "", "EnvironmentSet", args, nil) + return c.call("EnvironmentSet", args, nil) +} + +// EnvironmentUnset sets the given key-value pairs in the environment. +func (c *Client) EnvironmentUnset(keys ...string) error { + args := params.EnvironmentUnset{Keys: keys} + return c.call("EnvironmentUnset", args, nil) } // SetEnvironAgentVersion sets the environment agent-version setting // to the given value. func (c *Client) SetEnvironAgentVersion(version version.Number) error { args := params.SetEnvironAgentVersion{Version: version} - return c.st.Call("Client", "", "SetEnvironAgentVersion", args, nil) + return c.call("SetEnvironAgentVersion", args, nil) } // FindTools returns a List containing all tools matching the specified parameters. @@ -440,7 +488,7 @@ Arch: arch, Series: series, } - err = c.st.Call("Client", "", "FindTools", args, &result) + err = c.call("FindTools", args, &result) return result, err } @@ -449,7 +497,7 @@ func (c *Client) RunOnAllMachines(commands string, timeout time.Duration) ([]params.RunResult, error) { var results params.RunResults args := params.RunParams{Commands: commands, Timeout: timeout} - err := c.st.Call("Client", "", "RunOnAllMachines", args, &results) + err := c.call("RunOnAllMachines", args, &results) return results.Results, err } @@ -457,7 +505,7 @@ // provided in the machines, services and units slices. func (c *Client) Run(run params.RunParams) ([]params.RunResult, error) { var results params.RunResults - err := c.st.Call("Client", "", "Run", run, &results) + err := c.call("Run", run, &results) return results.Results, err } @@ -466,7 +514,7 @@ // will fail if there are any manually-provisioned non-manager machines // in state. func (c *Client) DestroyEnvironment() error { - return c.st.Call("Client", "", "DestroyEnvironment", nil, nil) + return c.call("DestroyEnvironment", nil, nil) } // AddLocalCharm prepares the given charm with a local: schema in its @@ -557,7 +605,7 @@ // client-side API. func (c *Client) AddCharm(curl *charm.URL) error { args := params.CharmURL{URL: curl.String()} - return c.st.Call("Client", "", "AddCharm", args, nil) + return c.call("AddCharm", args, nil) } func (c *Client) UploadTools( diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/common/apiaddresser.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/common/apiaddresser.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/common/apiaddresser.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/common/apiaddresser.go 2014-03-27 15:48:42.000000000 +0000 @@ -0,0 +1,71 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package common + +import ( + "launchpad.net/juju-core/instance" + "launchpad.net/juju-core/state/api/base" + "launchpad.net/juju-core/state/api/params" + "launchpad.net/juju-core/state/api/watcher" +) + +// APIAddresser provides common client-side API +// functions to call into apiserver.common.APIAddresser +type APIAddresser struct { + facadeName string + caller base.Caller +} + +// NewAPIAddresser returns a new APIAddresser that makes API calls +// using caller and the specified facade name. +func NewAPIAddresser(facadeName string, caller base.Caller) *APIAddresser { + return &APIAddresser{ + facadeName: facadeName, + caller: caller, + } +} + +// APIAddresses returns the list of addresses used to connect to the API. +func (a *APIAddresser) APIAddresses() ([]string, error) { + var result params.StringsResult + err := a.caller.Call(a.facadeName, "", "APIAddresses", nil, &result) + if err != nil { + return nil, err + } + + if err := result.Error; err != nil { + return nil, err + } + return result.Result, nil +} + +// CACert returns the certificate used to validate the API and state connections. +func (a *APIAddresser) CACert() ([]byte, error) { + var result params.BytesResult + err := a.caller.Call(a.facadeName, "", "CACert", nil, &result) + if err != nil { + return nil, err + } + return result.Result, nil +} + +// APIHostPorts returns the host/port addresses of the API servers. +func (a *APIAddresser) APIHostPorts() ([][]instance.HostPort, error) { + var result params.APIHostPortsResult + err := a.caller.Call(a.facadeName, "", "APIHostPorts", nil, &result) + if err != nil { + return nil, err + } + return result.Servers, nil +} + +// APIHostPorts watches the host/port addresses of the API servers. +func (a *APIAddresser) WatchAPIHostPorts() (watcher.NotifyWatcher, error) { + var result params.NotifyWatchResult + err := a.caller.Call(a.facadeName, "", "WatchAPIHostPorts", nil, &result) + if err != nil { + return nil, err + } + return watcher.NewNotifyWatcher(a.caller, result), nil +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/common/environwatcher.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/common/environwatcher.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/common/environwatcher.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/common/environwatcher.go 2014-03-27 15:48:42.000000000 +0000 @@ -10,44 +10,37 @@ "launchpad.net/juju-core/state/api/watcher" ) -// EnvironWatcher provides common client side api functions -// to call into the apiserver.common.EnvironWatcher. +// EnvironWatcher provides common client-side API functions +// to call into apiserver.common.EnvironWatcher. type EnvironWatcher struct { - façadeName string + facadeName string caller base.Caller } -// NewEnvironWatcher creates a EnvironWatcher on the specified façade, +// NewEnvironWatcher creates a EnvironWatcher on the specified facade, // and uses this name when calling through the caller. -func NewEnvironWatcher(façadeName string, caller base.Caller) *EnvironWatcher { - return &EnvironWatcher{façadeName, caller} +func NewEnvironWatcher(facadeName string, caller base.Caller) *EnvironWatcher { + return &EnvironWatcher{facadeName, caller} } // WatchForEnvironConfigChanges return a NotifyWatcher waiting for the // environment configuration to change. func (e *EnvironWatcher) WatchForEnvironConfigChanges() (watcher.NotifyWatcher, error) { var result params.NotifyWatchResult - err := e.caller.Call(e.façadeName, "", "WatchForEnvironConfigChanges", nil, &result) + err := e.caller.Call(e.facadeName, "", "WatchForEnvironConfigChanges", nil, &result) if err != nil { return nil, err } - if err := result.Error; err != nil { - return nil, result.Error - } - w := watcher.NewNotifyWatcher(e.caller, result) - return w, nil + return watcher.NewNotifyWatcher(e.caller, result), nil } // EnvironConfig returns the current environment configuration. func (e *EnvironWatcher) EnvironConfig() (*config.Config, error) { var result params.EnvironConfigResult - err := e.caller.Call(e.façadeName, "", "EnvironConfig", nil, &result) + err := e.caller.Call(e.facadeName, "", "EnvironConfig", nil, &result) if err != nil { return nil, err } - if err := result.Error; err != nil { - return nil, err - } conf, err := config.New(config.NoDefaults, result.Config) if err != nil { return nil, err diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/common/testing/environwatcher.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/common/testing/environwatcher.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/common/testing/environwatcher.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/common/testing/environwatcher.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,104 +0,0 @@ -// Copyright 2014 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package testing - -import ( - jc "github.com/juju/testing/checkers" - gc "launchpad.net/gocheck" - - "launchpad.net/juju-core/environs" - "launchpad.net/juju-core/environs/config" - "launchpad.net/juju-core/state" - "launchpad.net/juju-core/state/api/watcher" - statetesting "launchpad.net/juju-core/state/testing" -) - -const ( - HasSecrets = true - NoSecrets = false -) - -type Façade interface { - WatchForEnvironConfigChanges() (watcher.NotifyWatcher, error) - EnvironConfig() (*config.Config, error) -} - -type EnvironWatcherTest struct { - facade Façade - st *state.State - backing *state.State - hasSecrets bool -} - -func NewEnvironWatcherTest( - facade Façade, - st *state.State, - backing *state.State, - hasSecrets bool) *EnvironWatcherTest { - return &EnvironWatcherTest{facade, st, backing, hasSecrets} -} - -func (s *EnvironWatcherTest) TestEnvironConfig(c *gc.C) { - envConfig, err := s.st.EnvironConfig() - c.Assert(err, gc.IsNil) - - conf, err := s.facade.EnvironConfig() - c.Assert(err, gc.IsNil) - - // If the facade doesn't have secrets, we need to replace the config - // values in our environment to compare against with the secrets replaced. - if !s.hasSecrets { - env, err := environs.New(envConfig) - c.Assert(err, gc.IsNil) - secretAttrs, err := env.Provider().SecretAttrs(envConfig) - c.Assert(err, gc.IsNil) - secrets := make(map[string]interface{}) - for key := range secretAttrs { - secrets[key] = "not available" - } - envConfig, err = envConfig.Apply(secrets) - c.Assert(err, gc.IsNil) - } - - c.Assert(conf, jc.DeepEquals, envConfig) -} - -func (s *EnvironWatcherTest) TestWatchForEnvironConfigChanges(c *gc.C) { - envConfig, err := s.st.EnvironConfig() - c.Assert(err, gc.IsNil) - - w, err := s.facade.WatchForEnvironConfigChanges() - c.Assert(err, gc.IsNil) - defer statetesting.AssertStop(c, w) - wc := statetesting.NewNotifyWatcherC(c, s.backing, w) - - // Initial event. - wc.AssertOneChange() - - // Change the environment configuration by updating an existing attribute, check it's detected. - newAttrs := map[string]interface{}{"logging-config": "juju=ERROR"} - err = s.st.UpdateEnvironConfig(newAttrs, nil, nil) - c.Assert(err, gc.IsNil) - wc.AssertOneChange() - - // Change the environment configuration by adding a new attribute, check it's detected. - newAttrs = map[string]interface{}{"foo": "bar"} - err = s.st.UpdateEnvironConfig(newAttrs, nil, nil) - c.Assert(err, gc.IsNil) - wc.AssertOneChange() - - // Change the environment configuration by removing an attribute, check it's detected. - err = s.st.UpdateEnvironConfig(map[string]interface{}{}, []string{"foo"}, nil) - c.Assert(err, gc.IsNil) - wc.AssertOneChange() - - // Change it back to the original config. - oldAttrs := map[string]interface{}{"logging-config": envConfig.AllAttrs()["logging-config"]} - err = s.st.UpdateEnvironConfig(oldAttrs, nil, nil) - c.Assert(err, gc.IsNil) - wc.AssertOneChange() - - statetesting.AssertStop(c, w) - wc.AssertClosed() -} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/deployer/deployer.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/deployer/deployer.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/deployer/deployer.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/deployer/deployer.go 2014-03-27 15:48:42.000000000 +0000 @@ -9,20 +9,31 @@ "launchpad.net/juju-core/state/api/params" ) +const deployerFacade = "Deployer" + // State provides access to the deployer worker's idea of the state. type State struct { caller base.Caller + *common.APIAddresser } // NewState creates a new State instance that makes API calls // through the given caller. func NewState(caller base.Caller) *State { - return &State{caller} + return &State{ + APIAddresser: common.NewAPIAddresser(deployerFacade, caller), + caller: caller, + } + +} + +func (st *State) call(method string, params, result interface{}) error { + return st.caller.Call(deployerFacade, "", method, params, result) } // unitLife returns the lifecycle state of the given unit. func (st *State) unitLife(tag string) (params.Life, error) { - return common.Life(st.caller, "Deployer", tag) + return common.Life(st.caller, deployerFacade, tag) } // Unit returns the unit with the given tag. @@ -49,27 +60,7 @@ // StateAddresses returns the list of addresses used to connect to the state. func (st *State) StateAddresses() ([]string, error) { var result params.StringsResult - err := st.caller.Call("Deployer", "", "StateAddresses", nil, &result) - if err != nil { - return nil, err - } - return result.Result, nil -} - -// APIAddresses returns the list of addresses used to connect to the API. -func (st *State) APIAddresses() ([]string, error) { - var result params.StringsResult - err := st.caller.Call("Deployer", "", "APIAddresses", nil, &result) - if err != nil { - return nil, err - } - return result.Result, nil -} - -// CACert returns the certificate used to validate the state connection. -func (st *State) CACert() ([]byte, error) { - var result params.BytesResult - err := st.caller.Call("Deployer", "", "CACert", nil, &result) + err := st.call("StateAddresses", nil, &result) if err != nil { return nil, err } @@ -79,6 +70,6 @@ // ConnectionInfo returns all the address information that the deployer task // needs in one call. func (st *State) ConnectionInfo() (result params.DeployerConnectionValues, err error) { - err = st.caller.Call("Deployer", "", "ConnectionInfo", nil, &result) + err = st.call("ConnectionInfo", nil, &result) return result, err } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/deployer/deployer_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/deployer/deployer_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/deployer/deployer_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/deployer/deployer_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -15,6 +15,7 @@ "launchpad.net/juju-core/state/api" "launchpad.net/juju-core/state/api/deployer" "launchpad.net/juju-core/state/api/params" + apitesting "launchpad.net/juju-core/state/api/testing" statetesting "launchpad.net/juju-core/state/testing" coretesting "launchpad.net/juju-core/testing" ) @@ -25,6 +26,7 @@ type deployerSuite struct { testing.JujuConnSuite + *apitesting.APIAddresserTests stateAPI *api.State @@ -44,8 +46,9 @@ func (s *deployerSuite) SetUpTest(c *gc.C) { s.JujuConnSuite.SetUpTest(c) s.stateAPI, s.machine = s.OpenAPIAsNewMachine(c, state.JobManageEnviron, state.JobHostUnits) + err := s.machine.SetAddresses(instance.NewAddresses([]string{"0.1.2.3"})) + c.Assert(err, gc.IsNil) - var err error // Create the needed services and relate them. s.service0 = s.AddTestingService(c, "mysql", s.AddTestingCharm(c, "mysql")) s.service1 = s.AddTestingService(c, "logging", s.AddTestingCharm(c, "logging")) @@ -69,6 +72,8 @@ // Create the deployer facade. s.st = s.stateAPI.Deployer() c.Assert(s.st, gc.NotNil) + + s.APIAddresserTests = apitesting.NewAPIAddresserTests(s.st, s.BackingState) } // Note: This is really meant as a unit-test, this isn't a test that @@ -246,25 +251,3 @@ c.Assert(err, gc.IsNil) c.Assert(addresses, gc.DeepEquals, stateAddresses) } - -func (s *deployerSuite) TestAPIAddresses(c *gc.C) { - addrs := []instance.Address{ - instance.NewAddress("0.1.2.3"), - } - err := s.machine.SetAddresses(addrs) - c.Assert(err, gc.IsNil) - - stateAddresses, err := s.State.APIAddressesFromMachines() - c.Assert(err, gc.IsNil) - c.Assert(len(stateAddresses), gc.Equals, 1) - - addresses, err := s.st.APIAddresses() - c.Assert(err, gc.IsNil) - c.Assert(addresses, gc.DeepEquals, stateAddresses) -} - -func (s *deployerSuite) TestCACert(c *gc.C) { - caCert, err := s.st.CACert() - c.Assert(err, gc.IsNil) - c.Assert(caCert, gc.DeepEquals, s.State.CACert()) -} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/deployer/machine.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/deployer/machine.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/deployer/machine.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/deployer/machine.go 2014-03-27 15:48:42.000000000 +0000 @@ -24,7 +24,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: m.tag}}, } - err := m.st.caller.Call("Deployer", "", "WatchUnits", args, &results) + err := m.st.call("WatchUnits", args, &results) if err != nil { return nil, err } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/deployer/unit.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/deployer/unit.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/deployer/unit.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/deployer/unit.go 2014-03-27 15:48:42.000000000 +0000 @@ -51,7 +51,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: u.tag}}, } - err := u.st.caller.Call("Deployer", "", "Remove", args, &result) + err := u.st.call("Remove", args, &result) if err != nil { return err } @@ -66,7 +66,7 @@ {Tag: u.tag, Password: password}, }, } - err := u.st.caller.Call("Deployer", "", "SetPasswords", args, &result) + err := u.st.call("SetPasswords", args, &result) if err != nil { return err } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/environment/environment_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/environment/environment_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/environment/environment_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/environment/environment_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -7,12 +7,12 @@ gc "launchpad.net/gocheck" jujutesting "launchpad.net/juju-core/juju/testing" - commontesting "launchpad.net/juju-core/state/api/common/testing" + apitesting "launchpad.net/juju-core/state/api/testing" ) type environmentSuite struct { jujutesting.JujuConnSuite - *commontesting.EnvironWatcherTest + *apitesting.EnvironWatcherTests } var _ = gc.Suite(&environmentSuite{}) @@ -25,6 +25,6 @@ environmentAPI := stateAPI.Environment() c.Assert(environmentAPI, gc.NotNil) - s.EnvironWatcherTest = commontesting.NewEnvironWatcherTest( - environmentAPI, s.State, s.BackingState, commontesting.NoSecrets) + s.EnvironWatcherTests = apitesting.NewEnvironWatcherTests( + environmentAPI, s.BackingState, apitesting.NoSecrets) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/firewaller/firewaller.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/firewaller/firewaller.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/firewaller/firewaller.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/firewaller/firewaller.go 2014-03-27 15:48:42.000000000 +0000 @@ -4,26 +4,35 @@ package firewaller import ( - "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/state/api/base" "launchpad.net/juju-core/state/api/common" "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/api/watcher" ) +const firewallerFacade = "Firewaller" + // State provides access to the Firewaller API facade. type State struct { caller base.Caller + *common.EnvironWatcher +} + +func (st *State) call(method string, params, result interface{}) error { + return st.caller.Call(firewallerFacade, "", method, params, result) } // NewState creates a new client-side Firewaller facade. func NewState(caller base.Caller) *State { - return &State{caller} + return &State{ + caller: caller, + EnvironWatcher: common.NewEnvironWatcher(firewallerFacade, caller), + } } // life requests the life cycle of the given entity from the server. func (st *State) life(tag string) (params.Life, error) { - return common.Life(st.caller, "Firewaller", tag) + return common.Life(st.caller, firewallerFacade, tag) } // Unit provides access to methods of a state.Unit through the facade. @@ -53,44 +62,12 @@ }, nil } -// WatchForEnvironConfigChanges return a NotifyWatcher waiting for the -// environment configuration to change. -func (st *State) WatchForEnvironConfigChanges() (watcher.NotifyWatcher, error) { - var result params.NotifyWatchResult - err := st.caller.Call("Firewaller", "", "WatchForEnvironConfigChanges", nil, &result) - if err != nil { - return nil, err - } - if err := result.Error; err != nil { - return nil, result.Error - } - w := watcher.NewNotifyWatcher(st.caller, result) - return w, nil -} - -// EnvironConfig returns the current environment configuration. -func (st *State) EnvironConfig() (*config.Config, error) { - var result params.EnvironConfigResult - err := st.caller.Call("Firewaller", "", "EnvironConfig", nil, &result) - if err != nil { - return nil, err - } - if err := result.Error; err != nil { - return nil, err - } - conf, err := config.New(config.NoDefaults, result.Config) - if err != nil { - return nil, err - } - return conf, nil -} - // WatchEnvironMachines returns a StringsWatcher that notifies of // changes to the life cycles of the top level machines in the current // environment. func (st *State) WatchEnvironMachines() (watcher.StringsWatcher, error) { var result params.StringsWatchResult - err := st.caller.Call("Firewaller", "", "WatchEnvironMachines", nil, &result) + err := st.call("WatchEnvironMachines", nil, &result) if err != nil { return nil, err } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/firewaller/machine.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/firewaller/machine.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/firewaller/machine.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/firewaller/machine.go 2014-03-27 15:48:42.000000000 +0000 @@ -24,7 +24,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: m.tag}}, } - err := m.st.caller.Call("Firewaller", "", "WatchUnits", args, &results) + err := m.st.call("WatchUnits", args, &results) if err != nil { return nil, err } @@ -46,7 +46,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: m.tag}}, } - err := m.st.caller.Call("Firewaller", "", "InstanceId", args, &results) + err := m.st.call("InstanceId", args, &results) if err != nil { return "", err } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/firewaller/service.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/firewaller/service.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/firewaller/service.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/firewaller/service.go 2014-03-27 15:48:42.000000000 +0000 @@ -33,7 +33,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: s.tag}}, } - err := s.st.caller.Call("Firewaller", "", "Watch", args, &results) + err := s.st.call("Watch", args, &results) if err != nil { return nil, err } @@ -75,7 +75,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: s.tag}}, } - err := s.st.caller.Call("Firewaller", "", "GetExposed", args, &results) + err := s.st.call("GetExposed", args, &results) if err != nil { return false, err } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/firewaller/state_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/firewaller/state_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/firewaller/state_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/firewaller/state_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -4,22 +4,24 @@ package firewaller_test import ( - jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" "launchpad.net/juju-core/instance" "launchpad.net/juju-core/state" + apitesting "launchpad.net/juju-core/state/api/testing" statetesting "launchpad.net/juju-core/state/testing" ) type stateSuite struct { firewallerSuite + *apitesting.EnvironWatcherTests } var _ = gc.Suite(&stateSuite{}) func (s *stateSuite) SetUpTest(c *gc.C) { s.firewallerSuite.SetUpTest(c) + s.EnvironWatcherTests = apitesting.NewEnvironWatcherTests(s.firewaller, s.BackingState, true) } func (s *stateSuite) TearDownTest(c *gc.C) { @@ -56,41 +58,4 @@ statetesting.AssertStop(c, w) wc.AssertClosed() -} - -func (s *stateSuite) TestEnvironConfig(c *gc.C) { - envConfig, err := s.State.EnvironConfig() - c.Assert(err, gc.IsNil) - - conf, err := s.firewaller.EnvironConfig() - c.Assert(err, gc.IsNil) - c.Assert(conf, jc.DeepEquals, envConfig) -} - -func (s *stateSuite) TestWatchForEnvironConfigChanges(c *gc.C) { - envConfig, err := s.State.EnvironConfig() - c.Assert(err, gc.IsNil) - - w, err := s.firewaller.WatchForEnvironConfigChanges() - c.Assert(err, gc.IsNil) - defer statetesting.AssertStop(c, w) - wc := statetesting.NewNotifyWatcherC(c, s.BackingState, w) - - // Initial event. - wc.AssertOneChange() - - // Change the environment configuration, check it's detected. - newAttrs := map[string]interface{}{"logging-config": "juju=ERROR"} - err = s.State.UpdateEnvironConfig(newAttrs, nil, nil) - c.Assert(err, gc.IsNil) - wc.AssertOneChange() - - // Change it back to the original config. - oldAttrs := map[string]interface{}{"logging-config": envConfig.AllAttrs()["logging-config"]} - err = s.State.UpdateEnvironConfig(oldAttrs, nil, nil) - c.Assert(err, gc.IsNil) - wc.AssertOneChange() - - statetesting.AssertStop(c, w) - wc.AssertClosed() } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/firewaller/unit.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/firewaller/unit.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/firewaller/unit.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/firewaller/unit.go 2014-03-27 15:48:42.000000000 +0000 @@ -49,7 +49,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: u.tag}}, } - err := u.st.caller.Call("Firewaller", "", "Watch", args, &results) + err := u.st.call("Watch", args, &results) if err != nil { return nil, err } @@ -89,7 +89,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: u.tag}}, } - err := u.st.caller.Call("Firewaller", "", "OpenedPorts", args, &results) + err := u.st.call("OpenedPorts", args, &results) if err != nil { return nil, err } @@ -110,7 +110,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: u.tag}}, } - err := u.st.caller.Call("Firewaller", "", "GetAssignedMachine", args, &results) + err := u.st.call("GetAssignedMachine", args, &results) if err != nil { return "", err } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/keymanager/client.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/keymanager/client.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/keymanager/client.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/keymanager/client.go 2014-03-27 15:48:42.000000000 +0000 @@ -14,6 +14,10 @@ st *api.State } +func (c *Client) call(method string, params, result interface{}) error { + return c.st.Call("KeyManager", "", method, params, result) +} + // NewClient returns a new keymanager client. func NewClient(st *api.State) *Client { return &Client{st} @@ -32,7 +36,7 @@ p.Entities.Entities[i] = params.Entity{Tag: userName} } results := new(params.StringsResults) - err := c.st.Call("KeyManager", "", "ListKeys", p, results) + err := c.call("ListKeys", p, results) return results.Results, err } @@ -40,7 +44,7 @@ func (c *Client) AddKeys(user string, keys ...string) ([]params.ErrorResult, error) { p := params.ModifyUserSSHKeys{User: user, Keys: keys} results := new(params.ErrorResults) - err := c.st.Call("KeyManager", "", "AddKeys", p, results) + err := c.call("AddKeys", p, results) return results.Results, err } @@ -48,7 +52,7 @@ func (c *Client) DeleteKeys(user string, keys ...string) ([]params.ErrorResult, error) { p := params.ModifyUserSSHKeys{User: user, Keys: keys} results := new(params.ErrorResults) - err := c.st.Call("KeyManager", "", "DeleteKeys", p, results) + err := c.call("DeleteKeys", p, results) return results.Results, err } @@ -56,6 +60,6 @@ func (c *Client) ImportKeys(user string, keyIds ...string) ([]params.ErrorResult, error) { p := params.ModifyUserSSHKeys{User: user, Keys: keyIds} results := new(params.ErrorResults) - err := c.st.Call("KeyManager", "", "ImportKeys", p, results) + err := c.call("ImportKeys", p, results) return results.Results, err } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/keyupdater/authorisedkeys.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/keyupdater/authorisedkeys.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/keyupdater/authorisedkeys.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/keyupdater/authorisedkeys.go 2014-03-27 15:48:42.000000000 +0000 @@ -16,6 +16,10 @@ caller base.Caller } +func (st *State) call(method string, params, result interface{}) error { + return st.caller.Call("KeyUpdater", "", method, params, result) +} + // NewState returns a version of the state that provides functionality required by the worker. func NewState(caller base.Caller) *State { return &State{caller} @@ -27,7 +31,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: machineTag}}, } - err := st.caller.Call("KeyUpdater", "", "AuthorisedKeys", args, &results) + err := st.call("AuthorisedKeys", args, &results) if err != nil { // TODO: Not directly tested return nil, err @@ -50,7 +54,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: machineTag}}, } - err := st.caller.Call("KeyUpdater", "", "WatchAuthorisedKeys", args, &results) + err := st.call("WatchAuthorisedKeys", args, &results) if err != nil { // TODO: Not directly tested return nil, err diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/logger/logger.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/logger/logger.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/logger/logger.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/logger/logger.go 2014-03-27 15:48:42.000000000 +0000 @@ -16,6 +16,10 @@ caller base.Caller } +func (st *State) call(method string, params, result interface{}) error { + return st.caller.Call("Logger", "", method, params, result) +} + // NewState returns a version of the state that provides functionality // required by the logger worker. func NewState(caller base.Caller) *State { @@ -29,7 +33,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: agentTag}}, } - err := st.caller.Call("Logger", "", "LoggingConfig", args, &results) + err := st.call("LoggingConfig", args, &results) if err != nil { // TODO: Not directly tested return "", err @@ -52,7 +56,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: agentTag}}, } - err := st.caller.Call("Logger", "", "WatchLoggingConfig", args, &results) + err := st.call("WatchLoggingConfig", args, &results) if err != nil { // TODO: Not directly tested return nil, err diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/machiner/machine.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/machiner/machine.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/machiner/machine.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/machiner/machine.go 2014-03-27 15:48:42.000000000 +0000 @@ -42,11 +42,11 @@ func (m *Machine) SetStatus(status params.Status, info string, data params.StatusData) error { var result params.ErrorResults args := params.SetStatus{ - Entities: []params.SetEntityStatus{ + Entities: []params.EntityStatus{ {Tag: m.tag, Status: status, Info: info, Data: data}, }, } - err := m.st.caller.Call("Machiner", "", "SetStatus", args, &result) + err := m.st.call("SetStatus", args, &result) if err != nil { return err } @@ -61,7 +61,7 @@ {Tag: m.Tag(), Addresses: addresses}, }, } - err := m.st.caller.Call("Machiner", "", "SetMachineAddresses", args, &result) + err := m.st.call("SetMachineAddresses", args, &result) if err != nil { return err } @@ -75,7 +75,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: m.tag}}, } - err := m.st.caller.Call("Machiner", "", "EnsureDead", args, &result) + err := m.st.call("EnsureDead", args, &result) if err != nil { return err } @@ -88,7 +88,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: m.tag}}, } - err := m.st.caller.Call("Machiner", "", "Watch", args, &results) + err := m.st.call("Watch", args, &results) if err != nil { return nil, err } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/machiner/machiner.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/machiner/machiner.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/machiner/machiner.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/machiner/machiner.go 2014-03-27 15:48:42.000000000 +0000 @@ -9,19 +9,30 @@ "launchpad.net/juju-core/state/api/params" ) +const machinerFacade = "Machiner" + // State provides access to the Machiner API facade. type State struct { caller base.Caller + *common.APIAddresser +} + +func (st *State) call(method string, params, result interface{}) error { + return st.caller.Call(machinerFacade, "", method, params, result) } // NewState creates a new client-side Machiner facade. func NewState(caller base.Caller) *State { - return &State{caller} + return &State{ + caller: caller, + APIAddresser: common.NewAPIAddresser(machinerFacade, caller), + } + } // machineLife requests the lifecycle of the given machine from the server. func (st *State) machineLife(tag string) (params.Life, error) { - return common.Life(st.caller, "Machiner", tag) + return common.Life(st.caller, machinerFacade, tag) } // Machine provides access to methods of a state.Machine through the facade. diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/machiner/machiner_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/machiner/machiner_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/machiner/machiner_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/machiner/machiner_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -16,6 +16,7 @@ "launchpad.net/juju-core/state/api" "launchpad.net/juju-core/state/api/machiner" "launchpad.net/juju-core/state/api/params" + apitesting "launchpad.net/juju-core/state/api/testing" statetesting "launchpad.net/juju-core/state/testing" coretesting "launchpad.net/juju-core/testing" ) @@ -26,6 +27,8 @@ type machinerSuite struct { testing.JujuConnSuite + *apitesting.APIAddresserTests + st *api.State machine *state.Machine @@ -36,10 +39,16 @@ func (s *machinerSuite) SetUpTest(c *gc.C) { s.JujuConnSuite.SetUpTest(c) + m, err := s.State.AddMachine("quantal", state.JobManageEnviron) + c.Assert(err, gc.IsNil) + err = m.SetAddresses(instance.NewAddresses([]string{"127.0.0.1"})) + c.Assert(err, gc.IsNil) + s.st, s.machine = s.OpenAPIAsNewMachine(c) // Create the machiner API facade. s.machiner = s.st.Machiner() c.Assert(s.machiner, gc.NotNil) + s.APIAddresserTests = apitesting.NewAPIAddresserTests(s.machiner, s.BackingState) } func (s *machinerSuite) TestMachineAndMachineTag(c *gc.C) { @@ -48,13 +57,13 @@ c.Assert(err, jc.Satisfies, params.IsCodeUnauthorized) c.Assert(machine, gc.IsNil) - machine, err = s.machiner.Machine("machine-0") + machine, err = s.machiner.Machine("machine-1") c.Assert(err, gc.IsNil) - c.Assert(machine.Tag(), gc.Equals, "machine-0") + c.Assert(machine.Tag(), gc.Equals, "machine-1") } func (s *machinerSuite) TestSetStatus(c *gc.C) { - machine, err := s.machiner.Machine("machine-0") + machine, err := s.machiner.Machine("machine-1") c.Assert(err, gc.IsNil) status, info, data, err := s.machine.Status() @@ -76,7 +85,7 @@ func (s *machinerSuite) TestEnsureDead(c *gc.C) { c.Assert(s.machine.Life(), gc.Equals, state.Alive) - machine, err := s.machiner.Machine("machine-0") + machine, err := s.machiner.Machine("machine-1") c.Assert(err, gc.IsNil) err = machine.EnsureDead() @@ -98,12 +107,12 @@ c.Assert(err, jc.Satisfies, errors.IsNotFoundError) err = machine.EnsureDead() - c.Assert(err, gc.ErrorMatches, "machine 0 not found") + c.Assert(err, gc.ErrorMatches, "machine 1 not found") c.Assert(err, jc.Satisfies, params.IsCodeNotFound) } func (s *machinerSuite) TestRefresh(c *gc.C) { - machine, err := s.machiner.Machine("machine-0") + machine, err := s.machiner.Machine("machine-1") c.Assert(err, gc.IsNil) c.Assert(machine.Life(), gc.Equals, params.Alive) @@ -117,7 +126,7 @@ } func (s *machinerSuite) TestSetMachineAddresses(c *gc.C) { - machine, err := s.machiner.Machine("machine-0") + machine, err := s.machiner.Machine("machine-1") c.Assert(err, gc.IsNil) addr := s.machine.Addresses() @@ -136,7 +145,7 @@ } func (s *machinerSuite) TestWatch(c *gc.C) { - machine, err := s.machiner.Machine("machine-0") + machine, err := s.machiner.Machine("machine-1") c.Assert(err, gc.IsNil) c.Assert(machine.Life(), gc.Equals, params.Alive) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/params/internal.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/params/internal.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/params/internal.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/params/internal.go 2014-03-27 15:48:42.000000000 +0000 @@ -182,7 +182,6 @@ // EnvironConfigResult holds environment configuration or an error. type EnvironConfigResult struct { - Error *Error Config EnvironConfig } @@ -316,25 +315,28 @@ Machines []MachineSetProvisioned } -// SetEntityStatus holds an entity tag, status and extra info. -type SetEntityStatus struct { +// EntityStatus holds an entity tag, status and extra info. +type EntityStatus struct { Tag string Status Status Info string Data StatusData } -// SetStatus holds the parameters for making a SetStatus call. +// SetStatus holds the parameters for making a SetStatus/UpdateStatus call. type SetStatus struct { - Entities []SetEntityStatus + Entities []EntityStatus } // StatusResult holds an entity status, extra information, or an // error. type StatusResult struct { Error *Error + Id string + Life Life Status Status Info string + Data StatusData } // StatusResults holds multiple status results. @@ -525,8 +527,15 @@ Error string } -// RunResults is used to return the slice of results. Api server side calls +// RunResults is used to return the slice of results. API server side calls // need to return single structure values. type RunResults struct { Results []RunResult } + +// APIHostPortsResult holds the result of an APIHostPorts +// call. Each element in the top level slice holds +// the addresses for one API server. +type APIHostPortsResult struct { + Servers [][]instance.HostPort +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/params/params.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/params/params.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/params/params.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/params/params.go 2014-03-27 15:48:42.000000000 +0000 @@ -149,6 +149,10 @@ ConfigYAML string // Takes precedence over config if both are present. Constraints constraints.Value ToMachineSpec string + // The following fields are supported from 1.17.7 onwards and + // ignored before that. + IncludeNetworks []string + ExcludeNetworks []string } // ServiceUpdate holds the parameters for making the ServiceUpdate call. @@ -453,14 +457,32 @@ Id interface{} } +// StateServingInfo holds information needed by a state +// server. +type StateServingInfo struct { + APIPort int + StatePort int + Cert string + PrivateKey string + // this will be passed as the KeyFile argument to MongoDB + SharedSecret string +} + // MachineInfo holds the information about a Machine // that is watched by StateWatcher. type MachineInfo struct { - Id string `bson:"_id"` - InstanceId string - Status Status - StatusInfo string - StatusData StatusData + Id string `bson:"_id"` + InstanceId string + Status Status + StatusInfo string + StatusData StatusData + Life Life + Series string + SupportedContainers []instance.ContainerType + SupportedContainersKnown bool + HardwareCharacteristics *instance.HardwareCharacteristics `json:",omitempty"` + Jobs []MachineJob + Addresses []instance.Address } func (i *MachineInfo) EntityId() EntityId { @@ -582,6 +604,12 @@ Config map[string]interface{} } +// EnvironmentUnset contains the arguments for EnvironmentUnset client API +// call. +type EnvironmentUnset struct { + Keys []string +} + // SetEnvironAgentVersion contains the arguments for // SetEnvironAgentVersion client API call. type SetEnvironAgentVersion struct { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/params/params_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/params/params_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/params/params_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/params/params_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -35,13 +35,19 @@ about: "MachineInfo Delta", value: params.Delta{ Entity: ¶ms.MachineInfo{ - Id: "Benji", - InstanceId: "Shazam", - Status: "error", - StatusInfo: "foo", + Id: "Benji", + InstanceId: "Shazam", + Status: "error", + StatusInfo: "foo", + Life: params.Alive, + Series: "trusty", + SupportedContainers: []instance.ContainerType{instance.LXC}, + Jobs: []params.MachineJob{state.JobManageEnviron.ToParams()}, + Addresses: []instance.Address{}, + HardwareCharacteristics: &instance.HardwareCharacteristics{}, }, }, - json: `["machine","change",{"Id":"Benji","InstanceId":"Shazam","Status":"error","StatusInfo":"foo","StatusData":null}]`, + json: `["machine","change",{"Id":"Benji","InstanceId":"Shazam","Status":"error","StatusInfo":"foo","StatusData":null,"Life":"alive","Series":"trusty","SupportedContainers":["lxc"],"SupportedContainersKnown":false,"Jobs":["JobManageEnviron"],"Addresses":[],"HardwareCharacteristics":{}}]`, }, { about: "ServiceInfo Delta", value: params.Delta{ @@ -49,7 +55,7 @@ Name: "Benji", Exposed: true, CharmURL: "cs:quantal/name", - Life: params.Life(state.Dying.String()), + Life: params.Dying, OwnerTag: "test-owner", MinUnits: 42, Constraints: constraints.MustParse("arch=arm mem=1024M"), diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/provisioner/machine.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/provisioner/machine.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/provisioner/machine.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/provisioner/machine.go 2014-03-27 15:48:42.000000000 +0000 @@ -56,14 +56,14 @@ } // SetStatus sets the status of the machine. -func (m *Machine) SetStatus(status params.Status, info string) error { +func (m *Machine) SetStatus(status params.Status, info string, data params.StatusData) error { var result params.ErrorResults args := params.SetStatus{ - Entities: []params.SetEntityStatus{ - {Tag: m.tag, Status: status, Info: info}, + Entities: []params.EntityStatus{ + {Tag: m.tag, Status: status, Info: info, Data: data}, }, } - err := m.st.caller.Call("Provisioner", "", "SetStatus", args, &result) + err := m.st.call("SetStatus", args, &result) if err != nil { return err } @@ -76,7 +76,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: m.tag}}, } - err := m.st.caller.Call("Provisioner", "", "Status", args, &results) + err := m.st.call("Status", args, &results) if err != nil { return "", "", err } @@ -98,7 +98,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: m.tag}}, } - err := m.st.caller.Call("Provisioner", "", "Constraints", args, &results) + err := m.st.call("Constraints", args, &results) if err != nil { return nothing, err } @@ -119,7 +119,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: m.tag}}, } - err := m.st.caller.Call("Provisioner", "", "EnsureDead", args, &result) + err := m.st.call("EnsureDead", args, &result) if err != nil { return err } @@ -133,7 +133,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: m.tag}}, } - err := m.st.caller.Call("Provisioner", "", "Remove", args, &result) + err := m.st.call("Remove", args, &result) if err != nil { return err } @@ -149,7 +149,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: m.tag}}, } - err := m.st.caller.Call("Provisioner", "", "Series", args, &results) + err := m.st.call("Series", args, &results) if err != nil { return "", err } @@ -175,7 +175,7 @@ Characteristics: characteristics, }}, } - err := m.st.caller.Call("Provisioner", "", "SetProvisioned", args, &result) + err := m.st.call("SetProvisioned", args, &result) if err != nil { return err } @@ -189,7 +189,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: m.tag}}, } - err := m.st.caller.Call("Provisioner", "", "InstanceId", args, &results) + err := m.st.call("InstanceId", args, &results) if err != nil { return "", err } @@ -211,7 +211,7 @@ {Tag: m.tag, Password: password}, }, } - err := m.st.caller.Call("Provisioner", "", "SetPasswords", args, &result) + err := m.st.call("SetPasswords", args, &result) if err != nil { return err } @@ -240,7 +240,7 @@ {MachineTag: m.tag, ContainerType: string(ctype)}, }, } - err := m.st.caller.Call("Provisioner", "", "WatchContainers", args, &results) + err := m.st.call("WatchContainers", args, &results) if err != nil { return nil, err } @@ -264,7 +264,7 @@ {MachineTag: m.tag}, }, } - err := m.st.caller.Call("Provisioner", "", "WatchContainers", args, &results) + err := m.st.call("WatchContainers", args, &results) if err != nil { return nil, err } @@ -287,7 +287,7 @@ {MachineTag: m.tag, ContainerTypes: containerTypes}, }, } - err := m.st.caller.Call("Provisioner", "", "SetSupportedContainers", args, &results) + err := m.st.call("SetSupportedContainers", args, &results) if err != nil { return err } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/provisioner/provisioner.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/provisioner/provisioner.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/provisioner/provisioner.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/provisioner/provisioner.go 2014-03-27 15:48:42.000000000 +0000 @@ -6,6 +6,7 @@ import ( "fmt" + "launchpad.net/juju-core/names" "launchpad.net/juju-core/state/api/base" "launchpad.net/juju-core/state/api/common" "launchpad.net/juju-core/state/api/params" @@ -13,26 +14,31 @@ "launchpad.net/juju-core/tools" ) -const provisioner = "Provisioner" - // State provides access to the Machiner API facade. type State struct { *common.EnvironWatcher + *common.APIAddresser caller base.Caller } +const provisionerFacade = "Provisioner" + // NewState creates a new client-side Machiner facade. func NewState(caller base.Caller) *State { return &State{ - EnvironWatcher: common.NewEnvironWatcher(provisioner, caller), + EnvironWatcher: common.NewEnvironWatcher(provisionerFacade, caller), + APIAddresser: common.NewAPIAddresser(provisionerFacade, caller), + caller: caller} +} - caller: caller} +func (st *State) call(method string, params, result interface{}) error { + return st.caller.Call(provisionerFacade, "", method, params, result) } // machineLife requests the lifecycle of the given machine from the server. func (st *State) machineLife(tag string) (params.Life, error) { - return common.Life(st.caller, provisioner, tag) + return common.Life(st.caller, provisionerFacade, tag) } // Machine provides access to methods of a state.Machine through the facade. @@ -53,7 +59,7 @@ // the current environment. func (st *State) WatchEnvironMachines() (watcher.StringsWatcher, error) { var result params.StringsWatchResult - err := st.caller.Call(provisioner, "", "WatchEnvironMachines", nil, &result) + err := st.call("WatchEnvironMachines", nil, &result) if err != nil { return nil, err } @@ -64,30 +70,23 @@ return w, nil } -// StateAddresses returns the list of addresses used to connect to the state. -func (st *State) StateAddresses() ([]string, error) { - var result params.StringsResult - err := st.caller.Call(provisioner, "", "StateAddresses", nil, &result) +func (st *State) WatchMachineErrorRetry() (watcher.NotifyWatcher, error) { + var result params.NotifyWatchResult + err := st.call("WatchMachineErrorRetry", nil, &result) if err != nil { return nil, err } - return result.Result, nil -} - -// APIAddresses returns the list of addresses used to connect to the API. -func (st *State) APIAddresses() ([]string, error) { - var result params.StringsResult - err := st.caller.Call(provisioner, "", "APIAddresses", nil, &result) - if err != nil { - return nil, err + if err := result.Error; err != nil { + return nil, result.Error } - return result.Result, nil + w := watcher.NewNotifyWatcher(st.caller, result) + return w, nil } -// CACert returns the certificate used to validate the state connection. -func (st *State) CACert() ([]byte, error) { - var result params.BytesResult - err := st.caller.Call(provisioner, "", "CACert", nil, &result) +// StateAddresses returns the list of addresses used to connect to the state. +func (st *State) StateAddresses() ([]string, error) { + var result params.StringsResult + err := st.call("StateAddresses", nil, &result) if err != nil { return nil, err } @@ -100,7 +99,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: tag}}, } - err := st.caller.Call(provisioner, "", "Tools", args, &results) + err := st.call("Tools", args, &results) if err != nil { // TODO: Not directly tested return nil, err @@ -119,6 +118,28 @@ // ContainerConfig returns information from the environment config that are // needed for container cloud-init. func (st *State) ContainerConfig() (result params.ContainerConfig, err error) { - err = st.caller.Call(provisioner, "", "ContainerConfig", nil, &result) + err = st.call("ContainerConfig", nil, &result) return result, err } + +// MachinesWithTransientErrors returns a slice of machines and corresponding status information +// for those machines which have transient provisioning errors. +func (st *State) MachinesWithTransientErrors() ([]*Machine, []params.StatusResult, error) { + var results params.StatusResults + err := st.call("MachinesWithTransientErrors", nil, &results) + if err != nil { + return nil, nil, err + } + machines := make([]*Machine, len(results.Results)) + for i, status := range results.Results { + if status.Error != nil { + continue + } + machines[i] = &Machine{ + tag: names.MachineTag(status.Id), + life: status.Life, + st: st, + } + } + return machines, results.Results, nil +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/provisioner/provisioner_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/provisioner/provisioner_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/provisioner/provisioner_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/provisioner/provisioner_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -15,9 +15,9 @@ "launchpad.net/juju-core/juju/testing" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" - commontesting "launchpad.net/juju-core/state/api/common/testing" "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/api/provisioner" + apitesting "launchpad.net/juju-core/state/api/testing" statetesting "launchpad.net/juju-core/state/testing" coretesting "launchpad.net/juju-core/testing" "launchpad.net/juju-core/tools" @@ -31,7 +31,8 @@ type provisionerSuite struct { testing.JujuConnSuite - *commontesting.EnvironWatcherTest + *apitesting.EnvironWatcherTests + *apitesting.APIAddresserTests st *api.State machine *state.Machine @@ -55,12 +56,15 @@ c.Assert(err, gc.IsNil) s.st = s.OpenAPIAsMachine(c, s.machine.Tag(), password, "fake_nonce") c.Assert(s.st, gc.NotNil) + err = s.machine.SetAddresses(instance.NewAddresses([]string{"0.1.2.3"})) + c.Assert(err, gc.IsNil) // Create the provisioner API facade. s.provisioner = s.st.Provisioner() c.Assert(s.provisioner, gc.NotNil) - s.EnvironWatcherTest = commontesting.NewEnvironWatcherTest(s.provisioner, s.State, s.BackingState, commontesting.HasSecrets) + s.EnvironWatcherTests = apitesting.NewEnvironWatcherTests(s.provisioner, s.BackingState, apitesting.HasSecrets) + s.APIAddresserTests = apitesting.NewAPIAddresserTests(s.provisioner, s.BackingState) } func (s *provisionerSuite) TestMachineTagAndId(c *gc.C) { @@ -84,13 +88,51 @@ c.Assert(status, gc.Equals, params.StatusPending) c.Assert(info, gc.Equals, "") - err = apiMachine.SetStatus(params.StatusStarted, "blah") + err = apiMachine.SetStatus(params.StatusStarted, "blah", nil) c.Assert(err, gc.IsNil) status, info, err = apiMachine.Status() c.Assert(err, gc.IsNil) c.Assert(status, gc.Equals, params.StatusStarted) c.Assert(info, gc.Equals, "blah") + _, _, data, err := s.machine.Status() + c.Assert(err, gc.IsNil) + c.Assert(data, gc.HasLen, 0) +} + +func (s *provisionerSuite) TestGetSetStatusWithData(c *gc.C) { + apiMachine, err := s.provisioner.Machine(s.machine.Tag()) + c.Assert(err, gc.IsNil) + + err = apiMachine.SetStatus(params.StatusError, "blah", params.StatusData{"foo": "bar"}) + c.Assert(err, gc.IsNil) + + status, info, err := apiMachine.Status() + c.Assert(err, gc.IsNil) + c.Assert(status, gc.Equals, params.StatusError) + c.Assert(info, gc.Equals, "blah") + _, _, data, err := s.machine.Status() + c.Assert(err, gc.IsNil) + c.Assert(data, gc.DeepEquals, params.StatusData{"foo": "bar"}) +} + +func (s *provisionerSuite) TestMachinesWithTransientErrors(c *gc.C) { + machine, err := s.State.AddMachine("quantal", state.JobHostUnits) + c.Assert(err, gc.IsNil) + err = machine.SetStatus(params.StatusError, "blah", params.StatusData{"transient": true}) + c.Assert(err, gc.IsNil) + machines, info, err := s.provisioner.MachinesWithTransientErrors() + c.Assert(err, gc.IsNil) + c.Assert(machines, gc.HasLen, 1) + c.Assert(machines[0].Id(), gc.Equals, "1") + c.Assert(info, gc.HasLen, 1) + c.Assert(info[0], gc.DeepEquals, params.StatusResult{ + Id: "1", + Life: "alive", + Status: "error", + Info: "blah", + Data: params.StatusData{"transient": true}, + }) } func (s *provisionerSuite) TestEnsureDeadAndRemove(c *gc.C) { @@ -250,7 +292,7 @@ // Change something other than the containers and make sure it's // not detected. - err = apiMachine.SetStatus(params.StatusStarted, "not really") + err = apiMachine.SetStatus(params.StatusStarted, "not really", nil) c.Assert(err, gc.IsNil) wc.AssertNoChange() @@ -338,20 +380,6 @@ c.Assert(addresses, gc.DeepEquals, stateAddresses) } -func (s *provisionerSuite) TestAPIAddresses(c *gc.C) { - err := s.machine.SetAddresses([]instance.Address{ - instance.NewAddress("0.1.2.3"), - }) - c.Assert(err, gc.IsNil) - - apiAddresses, err := s.State.APIAddressesFromMachines() - c.Assert(err, gc.IsNil) - - addresses, err := s.provisioner.APIAddresses() - c.Assert(err, gc.IsNil) - c.Assert(addresses, gc.DeepEquals, apiAddresses) -} - func (s *provisionerSuite) TestContainerConfig(c *gc.C) { result, err := s.provisioner.ContainerConfig() c.Assert(err, gc.IsNil) @@ -360,12 +388,6 @@ c.Assert(result.SSLHostnameVerification, jc.IsTrue) } -func (s *provisionerSuite) TestCACert(c *gc.C) { - caCert, err := s.provisioner.CACert() - c.Assert(err, gc.IsNil) - c.Assert(caCert, gc.DeepEquals, s.State.CACert()) -} - func (s *provisionerSuite) TestToolsWrongMachine(c *gc.C) { tools, err := s.provisioner.Tools("42") c.Assert(err, gc.ErrorMatches, "permission denied") diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/rsyslog/rsyslog_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/rsyslog/rsyslog_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/rsyslog/rsyslog_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/rsyslog/rsyslog_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -7,12 +7,12 @@ gc "launchpad.net/gocheck" jujutesting "launchpad.net/juju-core/juju/testing" - commontesting "launchpad.net/juju-core/state/api/common/testing" + apitesting "launchpad.net/juju-core/state/api/testing" ) type rsyslogSuite struct { jujutesting.JujuConnSuite - *commontesting.EnvironWatcherTest + *apitesting.EnvironWatcherTests } var _ = gc.Suite(&rsyslogSuite{}) @@ -24,11 +24,10 @@ rsyslogAPI := stateAPI.Rsyslog() c.Assert(rsyslogAPI, gc.NotNil) - s.EnvironWatcherTest = commontesting.NewEnvironWatcherTest( + s.EnvironWatcherTests = apitesting.NewEnvironWatcherTests( rsyslogAPI, - s.State, s.BackingState, - commontesting.NoSecrets, + apitesting.NoSecrets, ) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/testing/apiaddresser.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/testing/apiaddresser.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/testing/apiaddresser.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/testing/apiaddresser.go 2014-03-27 15:48:42.000000000 +0000 @@ -0,0 +1,101 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package testing + +import ( + gc "launchpad.net/gocheck" + + "launchpad.net/juju-core/instance" + "launchpad.net/juju-core/state" + "launchpad.net/juju-core/state/api/watcher" + statetesting "launchpad.net/juju-core/state/testing" +) + +type APIAddresserTests struct { + state *state.State + facade APIAddresserFacade +} + +func NewAPIAddresserTests(facade APIAddresserFacade, st *state.State) *APIAddresserTests { + return &APIAddresserTests{ + state: st, + facade: facade, + } +} + +type APIAddresserFacade interface { + APIAddresses() ([]string, error) + CACert() ([]byte, error) + APIHostPorts() ([][]instance.HostPort, error) + WatchAPIHostPorts() (watcher.NotifyWatcher, error) +} + +func (s *APIAddresserTests) TestAPIAddresses(c *gc.C) { + apiAddresses, err := s.state.APIAddressesFromMachines() + c.Assert(err, gc.IsNil) + + addresses, err := s.facade.APIAddresses() + c.Assert(err, gc.IsNil) + c.Assert(addresses, gc.DeepEquals, apiAddresses) +} + +func (s *APIAddresserTests) TestAPIHostPorts(c *gc.C) { + expectServerAddrs := [][]instance.HostPort{{{ + Address: instance.NewAddress("0.1.2.24"), + Port: 999, + }, { + Address: instance.NewAddress("example.com"), + Port: 1234, + }}, {{ + Address: instance.Address{ + Value: "2001:DB8::1", + Type: instance.Ipv6Address, + NetworkName: "someNetwork", + NetworkScope: instance.NetworkCloudLocal, + }, + Port: 999, + }}} + + err := s.state.SetAPIHostPorts(expectServerAddrs) + c.Assert(err, gc.IsNil) + + serverAddrs, err := s.facade.APIHostPorts() + c.Assert(err, gc.IsNil) + c.Assert(serverAddrs, gc.DeepEquals, expectServerAddrs) +} + +func (s *APIAddresserTests) TestCACert(c *gc.C) { + caCert, err := s.facade.CACert() + c.Assert(err, gc.IsNil) + c.Assert(caCert, gc.DeepEquals, s.state.CACert()) +} + +func (s *APIAddresserTests) TestWatchAPIHostPorts(c *gc.C) { + expectServerAddrs := [][]instance.HostPort{{{ + Address: instance.NewAddress("0.1.2.3"), + Port: 1234, + }}} + err := s.state.SetAPIHostPorts(expectServerAddrs) + c.Assert(err, gc.IsNil) + + w, err := s.facade.WatchAPIHostPorts() + c.Assert(err, gc.IsNil) + defer statetesting.AssertStop(c, w) + + wc := statetesting.NewNotifyWatcherC(c, s.state, w) + + // Initial event. + wc.AssertOneChange() + + // Change the state addresses and check that we get a notification + expectServerAddrs[0][0].Value = "0.1.99.99" + + err = s.state.SetAPIHostPorts(expectServerAddrs) + c.Assert(err, gc.IsNil) + + wc.AssertOneChange() + + statetesting.AssertStop(c, w) + wc.AssertClosed() +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/testing/environwatcher.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/testing/environwatcher.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/testing/environwatcher.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/testing/environwatcher.go 2014-03-27 15:48:42.000000000 +0000 @@ -0,0 +1,106 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package testing + +import ( + jc "github.com/juju/testing/checkers" + gc "launchpad.net/gocheck" + + "launchpad.net/juju-core/environs" + "launchpad.net/juju-core/environs/config" + "launchpad.net/juju-core/state" + "launchpad.net/juju-core/state/api/watcher" + statetesting "launchpad.net/juju-core/state/testing" +) + +const ( + HasSecrets = true + NoSecrets = false +) + +type EnvironWatcherFacade interface { + WatchForEnvironConfigChanges() (watcher.NotifyWatcher, error) + EnvironConfig() (*config.Config, error) +} + +type EnvironWatcherTests struct { + facade EnvironWatcherFacade + state *state.State + hasSecrets bool +} + +func NewEnvironWatcherTests( + facade EnvironWatcherFacade, + st *state.State, + hasSecrets bool) *EnvironWatcherTests { + return &EnvironWatcherTests{ + facade: facade, + state: st, + hasSecrets: hasSecrets, + } +} + +func (s *EnvironWatcherTests) TestEnvironConfig(c *gc.C) { + envConfig, err := s.state.EnvironConfig() + c.Assert(err, gc.IsNil) + + conf, err := s.facade.EnvironConfig() + c.Assert(err, gc.IsNil) + + // If the facade doesn't have secrets, we need to replace the config + // values in our environment to compare against with the secrets replaced. + if !s.hasSecrets { + env, err := environs.New(envConfig) + c.Assert(err, gc.IsNil) + secretAttrs, err := env.Provider().SecretAttrs(envConfig) + c.Assert(err, gc.IsNil) + secrets := make(map[string]interface{}) + for key := range secretAttrs { + secrets[key] = "not available" + } + envConfig, err = envConfig.Apply(secrets) + c.Assert(err, gc.IsNil) + } + + c.Assert(conf, jc.DeepEquals, envConfig) +} + +func (s *EnvironWatcherTests) TestWatchForEnvironConfigChanges(c *gc.C) { + envConfig, err := s.state.EnvironConfig() + c.Assert(err, gc.IsNil) + + w, err := s.facade.WatchForEnvironConfigChanges() + c.Assert(err, gc.IsNil) + defer statetesting.AssertStop(c, w) + wc := statetesting.NewNotifyWatcherC(c, s.state, w) + + // Initial event. + wc.AssertOneChange() + + // Change the environment configuration by updating an existing attribute, check it's detected. + newAttrs := map[string]interface{}{"logging-config": "juju=ERROR"} + err = s.state.UpdateEnvironConfig(newAttrs, nil, nil) + c.Assert(err, gc.IsNil) + wc.AssertOneChange() + + // Change the environment configuration by adding a new attribute, check it's detected. + newAttrs = map[string]interface{}{"foo": "bar"} + err = s.state.UpdateEnvironConfig(newAttrs, nil, nil) + c.Assert(err, gc.IsNil) + wc.AssertOneChange() + + // Change the environment configuration by removing an attribute, check it's detected. + err = s.state.UpdateEnvironConfig(map[string]interface{}{}, []string{"foo"}, nil) + c.Assert(err, gc.IsNil) + wc.AssertOneChange() + + // Change it back to the original config. + oldAttrs := map[string]interface{}{"logging-config": envConfig.AllAttrs()["logging-config"]} + err = s.state.UpdateEnvironConfig(oldAttrs, nil, nil) + c.Assert(err, gc.IsNil) + wc.AssertOneChange() + + statetesting.AssertStop(c, w) + wc.AssertClosed() +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/charm.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/uniter/charm.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/charm.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/uniter/charm.go 2014-03-27 15:48:42.000000000 +0000 @@ -9,6 +9,7 @@ "launchpad.net/juju-core/charm" "launchpad.net/juju-core/state/api/params" + "launchpad.net/juju-core/utils" ) // This module implements a subset of the interface provided by @@ -35,7 +36,7 @@ args := params.CharmURLs{ URLs: []params.CharmURL{{URL: c.url}}, } - err := c.st.caller.Call("Uniter", "", apiCall, args, &results) + err := c.st.call(apiCall, args, &results) if err != nil { return "", err } @@ -59,12 +60,12 @@ // TODO(dimitern): 2013-09-06 bug 1221834 // Cache the result after getting it once for the same charm URL, // because it's immutable. -func (c *Charm) ArchiveURL() (*url.URL, bool, error) { +func (c *Charm) ArchiveURL() (*url.URL, utils.SSLHostnameVerification, error) { var results params.CharmArchiveURLResults args := params.CharmURLs{ URLs: []params.CharmURL{{URL: c.url}}, } - err := c.st.caller.Call("Uniter", "", "CharmArchiveURL", args, &results) + err := c.st.call("CharmArchiveURL", args, &results) if err != nil { return nil, false, err } @@ -79,7 +80,11 @@ if err != nil { return nil, false, err } - return archiveURL, result.DisableSSLHostnameVerification, nil + hostnameVerification := utils.VerifySSLHostnames + if result.DisableSSLHostnameVerification { + hostnameVerification = utils.NoVerifySSLHostnames + } + return archiveURL, hostnameVerification, nil } // ArchiveSha256 returns the SHA256 digest of the charm archive diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/charm_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/uniter/charm_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/charm_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/uniter/charm_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -4,11 +4,11 @@ package uniter_test import ( - jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" envtesting "launchpad.net/juju-core/environs/testing" "launchpad.net/juju-core/state/api/uniter" + "launchpad.net/juju-core/utils" ) type charmSuite struct { @@ -46,17 +46,17 @@ } func (s *charmSuite) TestArchiveURL(c *gc.C) { - archiveURL, disableSSLHostnameVerification, err := s.apiCharm.ArchiveURL() + archiveURL, hostnameVerification, err := s.apiCharm.ArchiveURL() c.Assert(err, gc.IsNil) c.Assert(archiveURL, gc.DeepEquals, s.wordpressCharm.BundleURL()) - c.Assert(disableSSLHostnameVerification, jc.IsFalse) + c.Assert(hostnameVerification, gc.Equals, utils.VerifySSLHostnames) envtesting.SetSSLHostnameVerification(c, s.State, false) - archiveURL, disableSSLHostnameVerification, err = s.apiCharm.ArchiveURL() + archiveURL, hostnameVerification, err = s.apiCharm.ArchiveURL() c.Assert(err, gc.IsNil) c.Assert(archiveURL, gc.DeepEquals, s.wordpressCharm.BundleURL()) - c.Assert(disableSSLHostnameVerification, jc.IsTrue) + c.Assert(hostnameVerification, gc.Equals, utils.NoVerifySSLHostnames) } func (s *charmSuite) TestArchiveSha256(c *gc.C) { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/relationunit.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/uniter/relationunit.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/relationunit.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/uniter/relationunit.go 2014-03-27 15:48:42.000000000 +0000 @@ -75,7 +75,7 @@ Unit: ru.unit.tag, }}, } - err := ru.st.caller.Call("Uniter", "", "EnterScope", args, &result) + err := ru.st.call("EnterScope", args, &result) if err != nil { return err } @@ -95,7 +95,7 @@ Unit: ru.unit.tag, }}, } - err := ru.st.caller.Call("Uniter", "", "LeaveScope", args, &result) + err := ru.st.call("LeaveScope", args, &result) if err != nil { return err } @@ -112,7 +112,7 @@ Unit: ru.unit.tag, }}, } - err := ru.st.caller.Call("Uniter", "", "ReadSettings", args, &results) + err := ru.st.call("ReadSettings", args, &results) if err != nil { return nil, err } @@ -143,7 +143,7 @@ RemoteUnit: tag, }}, } - err := ru.st.caller.Call("Uniter", "", "ReadRemoteSettings", args, &results) + err := ru.st.call("ReadRemoteSettings", args, &results) if err != nil { return nil, err } @@ -167,7 +167,7 @@ Unit: ru.unit.tag, }}, } - err := ru.st.caller.Call("Uniter", "", "WatchRelationUnits", args, &results) + err := ru.st.call("WatchRelationUnits", args, &results) if err != nil { return nil, err } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/service.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/uniter/service.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/service.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/uniter/service.go 2014-03-27 15:48:42.000000000 +0000 @@ -42,7 +42,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: s.tag}}, } - err := s.st.caller.Call("Uniter", "", "Watch", args, &results) + err := s.st.call("Watch", args, &results) if err != nil { return nil, err } @@ -64,7 +64,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: s.tag}}, } - err := s.st.caller.Call("Uniter", "", "WatchServiceRelations", args, &results) + err := s.st.call("WatchServiceRelations", args, &results) if err != nil { return nil, err } @@ -106,7 +106,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: s.tag}}, } - err := s.st.caller.Call("Uniter", "", "CharmURL", args, &results) + err := s.st.call("CharmURL", args, &results) if err != nil { return nil, false, err } @@ -134,7 +134,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: s.tag}}, } - err := s.st.caller.Call("Uniter", "", "GetOwnerTag", args, &result) + err := s.st.call("GetOwnerTag", args, &result) if err != nil { return "", err } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/settings.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/uniter/settings.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/settings.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/uniter/settings.go 2014-03-27 15:48:42.000000000 +0000 @@ -88,7 +88,7 @@ Settings: settingsCopy, }}, } - err := s.st.caller.Call("Uniter", "", "UpdateSettings", args, &result) + err := s.st.call("UpdateSettings", args, &result) if err != nil { return err } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/state_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/uniter/state_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/state_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/uniter/state_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -5,24 +5,22 @@ import ( gc "launchpad.net/gocheck" + + apitesting "launchpad.net/juju-core/state/api/testing" ) type stateSuite struct { uniterSuite + *apitesting.APIAddresserTests + *apitesting.EnvironWatcherTests } var _ = gc.Suite(&stateSuite{}) -func (s *stateSuite) TestAPIAddresses(c *gc.C) { - stateAPIAddresses, err := s.State.APIAddressesFromMachines() - c.Assert(err, gc.IsNil) - addresses, err := s.uniter.APIAddresses() - c.Assert(err, gc.IsNil) - c.Assert(addresses, gc.DeepEquals, stateAPIAddresses) - // testing.AddStateServerMachine creates a machine which does *not* - // match the values in the Environ Config, so these don't match - apiInfo := s.APIInfo(c) - c.Assert(addresses, gc.Not(gc.DeepEquals), apiInfo.Addrs) +func (s *stateSuite) SetUpTest(c *gc.C) { + s.uniterSuite.SetUpTest(c) + s.APIAddresserTests = apitesting.NewAPIAddresserTests(s.uniter, s.BackingState) + s.EnvironWatcherTests = apitesting.NewEnvironWatcherTests(s.uniter, s.BackingState, apitesting.NoSecrets) } func (s *stateSuite) TestProviderType(c *gc.C) { @@ -33,19 +31,3 @@ c.Assert(err, gc.IsNil) c.Assert(providerType, gc.DeepEquals, cfg.Type()) } - -type noStateServerSuite struct { - uniterSuite -} - -var _ = gc.Suite(&noStateServerSuite{}) - -func (s *noStateServerSuite) SetUpTest(c *gc.C) { - // avoid adding the state server machine. - s.setUpTest(c, false) -} - -func (s *noStateServerSuite) TestAPIAddressesFailure(c *gc.C) { - _, err := s.uniter.APIAddresses() - c.Assert(err, gc.ErrorMatches, "no state server machines found") -} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/uniter.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/uniter/uniter.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/uniter.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/uniter/uniter.go 2014-03-27 15:48:42.000000000 +0000 @@ -13,11 +13,12 @@ "launchpad.net/juju-core/state/api/params" ) -const uniter = "Uniter" +const uniterFacade = "Uniter" // State provides access to the Uniter API facade. type State struct { *common.EnvironWatcher + *common.APIAddresser caller base.Caller // unitTag contains the authenticated unit's tag. @@ -27,14 +28,20 @@ // NewState creates a new client-side Uniter facade. func NewState(caller base.Caller, authTag string) *State { return &State{ - EnvironWatcher: common.NewEnvironWatcher(uniter, caller), + EnvironWatcher: common.NewEnvironWatcher(uniterFacade, caller), + APIAddresser: common.NewAPIAddresser(uniterFacade, caller), caller: caller, - unitTag: authTag} + unitTag: authTag, + } +} + +func (st *State) call(method string, params, results interface{}) error { + return st.caller.Call(uniterFacade, "", method, params, results) } // life requests the lifecycle of the given entity from the server. func (st *State) life(tag string) (params.Life, error) { - return common.Life(st.caller, uniter, tag) + return common.Life(st.caller, uniterFacade, tag) } // relation requests relation information from the server. @@ -46,7 +53,7 @@ {Relation: relationTag, Unit: unitTag}, }, } - err := st.caller.Call(uniter, "", "Relation", args, &result) + err := st.call("Relation", args, &result) if err != nil { return nothing, err } @@ -92,7 +99,7 @@ // addresses implemented fully. See also LP bug 1221798. func (st *State) ProviderType() (string, error) { var result params.StringResult - err := st.caller.Call(uniter, "", "ProviderType", nil, &result) + err := st.call("ProviderType", nil, &result) if err != nil { return "", err } @@ -133,7 +140,7 @@ args := params.RelationIds{ RelationIds: []int{id}, } - err := st.caller.Call(uniter, "", "RelationById", args, &results) + err := st.call("RelationById", args, &results) if err != nil { return nil, err } @@ -156,7 +163,7 @@ // Environment returns the environment entity. func (st *State) Environment() (*Environment, error) { var result params.EnvironmentResult - err := st.caller.Call("Uniter", "", "CurrentEnvironment", nil, &result) + err := st.call("CurrentEnvironment", nil, &result) if params.IsCodeNotImplemented(err) { // Fall back to using the 1.16 API. return st.environment1dot16() @@ -177,7 +184,7 @@ // using an older API server that does not support CurrentEnvironment API call. func (st *State) environment1dot16() (*Environment, error) { var result params.StringResult - err := st.caller.Call("Uniter", "", "CurrentEnvironUUID", nil, &result) + err := st.call("CurrentEnvironUUID", nil, &result) if err != nil { return nil, err } @@ -188,16 +195,3 @@ uuid: result.Result, }, nil } - -// APIAddresses returns the list of addresses used to connect to the API. -func (st *State) APIAddresses() ([]string, error) { - var result params.StringsResult - err := st.caller.Call(uniter, "", "APIAddresses", nil, &result) - if err != nil { - return nil, err - } - if err := result.Error; err != nil { - return nil, err - } - return result.Result, nil -} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/uniter_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/uniter/uniter_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/uniter_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/uniter/uniter_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -11,7 +11,6 @@ "launchpad.net/juju-core/juju/testing" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api" - commontesting "launchpad.net/juju-core/state/api/common/testing" "launchpad.net/juju-core/state/api/uniter" coretesting "launchpad.net/juju-core/testing" "launchpad.net/juju-core/utils" @@ -101,17 +100,3 @@ c.Assert(err, gc.IsNil) c.Assert(ok, gc.Equals, inScope) } - -type uniterCommonSuite struct { - uniterSuite - - *commontesting.EnvironWatcherTest -} - -var _ = gc.Suite(&uniterCommonSuite{}) - -func (s *uniterCommonSuite) SetUpTest(c *gc.C) { - s.uniterSuite.SetUpTest(c) - - s.EnvironWatcherTest = commontesting.NewEnvironWatcherTest(s.uniter, s.State, s.BackingState, commontesting.NoSecrets) -} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/unit.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/uniter/unit.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/uniter/unit.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/uniter/unit.go 2014-03-27 15:48:42.000000000 +0000 @@ -58,11 +58,11 @@ func (u *Unit) SetStatus(status params.Status, info string, data params.StatusData) error { var result params.ErrorResults args := params.SetStatus{ - Entities: []params.SetEntityStatus{ + Entities: []params.EntityStatus{ {Tag: u.tag, Status: status, Info: info, Data: data}, }, } - err := u.st.caller.Call("Uniter", "", "SetStatus", args, &result) + err := u.st.call("SetStatus", args, &result) if err != nil { return err } @@ -76,7 +76,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: u.tag}}, } - err := u.st.caller.Call("Uniter", "", "EnsureDead", args, &result) + err := u.st.call("EnsureDead", args, &result) if err != nil { return err } @@ -89,7 +89,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: u.tag}}, } - err := u.st.caller.Call("Uniter", "", "Watch", args, &results) + err := u.st.call("Watch", args, &results) if err != nil { return nil, err } @@ -129,7 +129,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: u.tag}}, } - err := u.st.caller.Call("Uniter", "", "ConfigSettings", args, &results) + err := u.st.call("ConfigSettings", args, &results) if err != nil { return nil, err } @@ -163,7 +163,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: u.tag}}, } - err := u.st.caller.Call("Uniter", "", "Destroy", args, &result) + err := u.st.call("Destroy", args, &result) if err != nil { return err } @@ -176,7 +176,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: u.tag}}, } - err := u.st.caller.Call("Uniter", "", "DestroyAllSubordinates", args, &result) + err := u.st.call("DestroyAllSubordinates", args, &result) if err != nil { return err } @@ -192,7 +192,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: u.tag}}, } - err := u.st.caller.Call("Uniter", "", "Resolved", args, &results) + err := u.st.call("Resolved", args, &results) if err != nil { return "", err } @@ -216,7 +216,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: u.tag}}, } - err := u.st.caller.Call("Uniter", "", "GetPrincipal", args, &results) + err := u.st.call("GetPrincipal", args, &results) if err != nil { return false, err } @@ -237,7 +237,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: u.tag}}, } - err := u.st.caller.Call("Uniter", "", "HasSubordinates", args, &results) + err := u.st.call("HasSubordinates", args, &results) if err != nil { return false, err } @@ -264,7 +264,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: u.tag}}, } - err := u.st.caller.Call("Uniter", "", "PublicAddress", args, &results) + err := u.st.call("PublicAddress", args, &results) if err != nil { return "", err } @@ -289,7 +289,7 @@ {Tag: u.tag, Address: address}, }, } - err := u.st.caller.Call("Uniter", "", "SetPublicAddress", args, &result) + err := u.st.call("SetPublicAddress", args, &result) if err != nil { return err } @@ -309,7 +309,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: u.tag}}, } - err := u.st.caller.Call("Uniter", "", "PrivateAddress", args, &results) + err := u.st.call("PrivateAddress", args, &results) if err != nil { return "", err } @@ -334,7 +334,7 @@ {Tag: u.tag, Address: address}, }, } - err := u.st.caller.Call("Uniter", "", "SetPrivateAddress", args, &result) + err := u.st.call("SetPrivateAddress", args, &result) if err != nil { return err } @@ -353,7 +353,7 @@ {Tag: u.tag, Protocol: protocol, Port: number}, }, } - err := u.st.caller.Call("Uniter", "", "OpenPort", args, &result) + err := u.st.call("OpenPort", args, &result) if err != nil { return err } @@ -372,7 +372,7 @@ {Tag: u.tag, Protocol: protocol, Port: number}, }, } - err := u.st.caller.Call("Uniter", "", "ClosePort", args, &result) + err := u.st.call("ClosePort", args, &result) if err != nil { return err } @@ -390,7 +390,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: u.tag}}, } - err := u.st.caller.Call("Uniter", "", "CharmURL", args, &results) + err := u.st.call("CharmURL", args, &results) if err != nil { return nil, err } @@ -423,7 +423,7 @@ {Tag: u.tag, CharmURL: curl.String()}, }, } - err := u.st.caller.Call("Uniter", "", "SetCharmURL", args, &result) + err := u.st.call("SetCharmURL", args, &result) if err != nil { return err } @@ -436,7 +436,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: u.tag}}, } - err := u.st.caller.Call("Uniter", "", "ClearResolved", args, &result) + err := u.st.call("ClearResolved", args, &result) if err != nil { return err } @@ -452,7 +452,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: u.tag}}, } - err := u.st.caller.Call("Uniter", "", "WatchConfigSettings", args, &results) + err := u.st.call("WatchConfigSettings", args, &results) if err != nil { return nil, err } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/upgrader/upgrader.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/upgrader/upgrader.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/upgrader/upgrader.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/upgrader/upgrader.go 2014-03-27 15:48:42.000000000 +0000 @@ -10,6 +10,7 @@ "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/api/watcher" "launchpad.net/juju-core/tools" + "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" ) @@ -18,6 +19,10 @@ caller base.Caller } +func (st *State) call(method string, params, result interface{}) error { + return st.caller.Call("Upgrader", "", method, params, result) +} + // NewState returns a version of the state that provides functionality // required by the upgrader worker. func NewState(caller base.Caller) *State { @@ -35,7 +40,7 @@ Tools: ¶ms.Version{v}, }}, } - err := st.caller.Call("Upgrader", "", "SetTools", args, &results) + err := st.call("SetTools", args, &results) if err != nil { // TODO: Not directly tested return err @@ -48,7 +53,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: tag}}, } - err := st.caller.Call("Upgrader", "", "DesiredVersion", args, &results) + err := st.call("DesiredVersion", args, &results) if err != nil { // TODO: Not directly tested return version.Number{}, err @@ -70,12 +75,12 @@ // Tools returns the agent tools that should run on the given entity, // along with a flag whether to disable SSL hostname verification. -func (st *State) Tools(tag string) (*tools.Tools, bool, error) { +func (st *State) Tools(tag string) (*tools.Tools, utils.SSLHostnameVerification, error) { var results params.ToolsResults args := params.Entities{ Entities: []params.Entity{{Tag: tag}}, } - err := st.caller.Call("Upgrader", "", "Tools", args, &results) + err := st.call("Tools", args, &results) if err != nil { // TODO: Not directly tested return nil, false, err @@ -88,7 +93,11 @@ if err := result.Error; err != nil { return nil, false, err } - return result.Tools, result.DisableSSLHostnameVerification, nil + hostnameVerification := utils.VerifySSLHostnames + if result.DisableSSLHostnameVerification { + hostnameVerification = utils.NoVerifySSLHostnames + } + return result.Tools, hostnameVerification, nil } func (st *State) WatchAPIVersion(agentTag string) (watcher.NotifyWatcher, error) { @@ -96,7 +105,7 @@ args := params.Entities{ Entities: []params.Entity{{Tag: agentTag}}, } - err := st.caller.Call("Upgrader", "", "WatchAPIVersion", args, &results) + err := st.call("WatchAPIVersion", args, &results) if err != nil { // TODO: Not directly tested return nil, err diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/upgrader/upgrader_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/upgrader/upgrader_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/upgrader/upgrader_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/upgrader/upgrader_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -19,6 +19,7 @@ statetesting "launchpad.net/juju-core/state/testing" coretesting "launchpad.net/juju-core/testing" "launchpad.net/juju-core/tools" + "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" ) @@ -101,19 +102,19 @@ s.rawMachine.SetAgentVersion(cur) // Upgrader.Tools returns the *desired* set of tools, not the currently // running set. We want to be upgraded to cur.Version - stateTools, disableSSLHostnameVerification, err := s.st.Tools(s.rawMachine.Tag()) + stateTools, hostnameVerification, err := s.st.Tools(s.rawMachine.Tag()) c.Assert(err, gc.IsNil) c.Assert(stateTools.Version, gc.Equals, cur) c.Assert(stateTools.URL, gc.Not(gc.Equals), "") - c.Assert(disableSSLHostnameVerification, jc.IsFalse) + c.Assert(hostnameVerification, gc.Equals, utils.VerifySSLHostnames) envtesting.SetSSLHostnameVerification(c, s.State, false) - stateTools, disableSSLHostnameVerification, err = s.st.Tools(s.rawMachine.Tag()) + stateTools, hostnameVerification, err = s.st.Tools(s.rawMachine.Tag()) c.Assert(err, gc.IsNil) c.Assert(stateTools.Version, gc.Equals, cur) c.Assert(stateTools.URL, gc.Not(gc.Equals), "") - c.Assert(disableSSLHostnameVerification, jc.IsTrue) + c.Assert(hostnameVerification, gc.Equals, utils.NoVerifySSLHostnames) } func (s *machineUpgraderSuite) TestWatchAPIVersion(c *gc.C) { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/api/usermanager/client.go juju-core-1.17.7/src/launchpad.net/juju-core/state/api/usermanager/client.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/api/usermanager/client.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/api/usermanager/client.go 2014-03-27 15:48:42.000000000 +0000 @@ -14,6 +14,10 @@ st *api.State } +func (c *Client) call(method string, params, result interface{}) error { + return c.st.Call("UserManager", "", method, params, result) +} + func NewClient(st *api.State) *Client { return &Client{st} } @@ -26,7 +30,7 @@ u := params.EntityPassword{Tag: tag, Password: password} p := params.EntityPasswords{Changes: []params.EntityPassword{u}} results := new(params.ErrorResults) - err := c.st.Call("UserManager", "", "AddUser", p, results) + err := c.call("AddUser", p, results) if err != nil { return err } @@ -37,7 +41,7 @@ u := params.Entity{Tag: tag} p := params.Entities{Entities: []params.Entity{u}} results := new(params.ErrorResults) - err := c.st.Call("UserManager", "", "RemoveUser", p, results) + err := c.call("RemoveUser", p, results) if err != nil { return err } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/charmrevisionupdater/testing/suite.go juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/charmrevisionupdater/testing/suite.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/charmrevisionupdater/testing/suite.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/charmrevisionupdater/testing/suite.go 2014-03-27 15:48:42.000000000 +0000 @@ -88,10 +88,10 @@ } // AddService adds a service for the specified charm to state. -func (s *CharmSuite) AddService(c *gc.C, charmName, serviceName string) { +func (s *CharmSuite) AddService(c *gc.C, charmName, serviceName string, includeNetworks, excludeNetworks []string) { ch, ok := s.charms[charmName] c.Assert(ok, gc.Equals, true) - _, err := s.State.AddService(serviceName, "user-admin", ch) + _, err := s.State.AddService(serviceName, "user-admin", ch, includeNetworks, excludeNetworks) c.Assert(err, gc.IsNil) } @@ -127,12 +127,12 @@ // mysql is out of date s.AddCharmWithRevision(c, "mysql", 22) - s.AddService(c, "mysql", "mysql") + s.AddService(c, "mysql", "mysql", nil, nil) s.AddUnit(c, "mysql", "1") // wordpress is up to date s.AddCharmWithRevision(c, "wordpress", 26) - s.AddService(c, "wordpress", "wordpress") + s.AddService(c, "wordpress", "wordpress", nil, nil) s.AddUnit(c, "wordpress", "2") s.AddUnit(c, "wordpress", "2") // wordpress/0 has a version, wordpress/1 is unknown @@ -140,6 +140,6 @@ // varnish is a charm that does not have a version in the mock store. s.AddCharmWithRevision(c, "varnish", 5) - s.AddService(c, "varnish", "varnish") + s.AddService(c, "varnish", "varnish", nil, nil) s.AddUnit(c, "varnish", "3") } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/charms.go juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/charms.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/charms.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/charms.go 2014-03-27 15:48:42.000000000 +0000 @@ -197,10 +197,12 @@ } // We got it, now let's reserve a charm URL for it in state. archiveURL := &charm.URL{ - Schema: "local", - Series: series, - Name: archive.Meta().Name, - Revision: archive.Revision(), + Reference: charm.Reference{ + Schema: "local", + Name: archive.Meta().Name, + Revision: archive.Revision(), + }, + Series: series, } preparedURL, err := h.state.PrepareLocalCharmUpload(archiveURL) if err != nil { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/client/client.go juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/client/client.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/client/client.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/client/client.go 2014-03-27 15:48:42.000000000 +0000 @@ -38,6 +38,8 @@ resources *common.Resources client *Client dataDir string + // statusSetter provides common methods for updating an entity's provisioning status. + statusSetter *common.StatusSetter } // Client serves client-specific API methods. @@ -48,10 +50,11 @@ // NewAPI creates a new instance of the Client API. func NewAPI(st *state.State, resources *common.Resources, authorizer common.Authorizer, datadir string) *API { r := &API{ - state: st, - auth: authorizer, - resources: resources, - dataDir: datadir, + state: st, + auth: authorizer, + resources: resources, + dataDir: datadir, + statusSetter: common.NewStatusSetter(st, common.AuthAlways(true)), } r.client = &Client{ api: r, @@ -279,16 +282,25 @@ _, err = juju.DeployService(c.api.state, juju.DeployServiceParams{ - ServiceName: args.ServiceName, - Charm: ch, - NumUnits: args.NumUnits, - ConfigSettings: settings, - Constraints: args.Constraints, - ToMachineSpec: args.ToMachineSpec, + ServiceName: args.ServiceName, + Charm: ch, + NumUnits: args.NumUnits, + ConfigSettings: settings, + Constraints: args.Constraints, + ToMachineSpec: args.ToMachineSpec, + IncludeNetworks: args.IncludeNetworks, + ExcludeNetworks: args.ExcludeNetworks, }) return err } +// ServiceDeployWithNetworks works exactly like ServiceDeploy, but +// allows specifying networks to include or exclude on the machine +// where the charm gets deployed. +func (c *Client) ServiceDeployWithNetworks(args params.ServiceDeploy) error { + return c.ServiceDeploy(args) +} + // ServiceUpdate updates the service attributes, including charm URL, // minimum number of units, settings and constraints. // All parameters in params.ServiceUpdate except the service name are optional. @@ -799,13 +811,19 @@ } return nil } - // TODO(waigani) 2014-3-11 #1167616 // Add a txn retry loop to ensure that the settings on disk have not // changed underneath us. - return c.api.state.UpdateEnvironConfig(args.Config, nil, checkAgentVersion) +} +// EnvironmentUnset implements the server-side part of the +// set-environment CLI command. +func (c *Client) EnvironmentUnset(args params.EnvironmentUnset) error { + // TODO(waigani) 2014-3-11 #1167616 + // Add a txn retry loop to ensure that the settings on disk have not + // changed underneath us. + return c.api.state.UpdateEnvironConfig(nil, args.Keys, nil) } // SetEnvironAgentVersion sets the environment agent version. @@ -947,3 +965,14 @@ } return charm.Quote(fmt.Sprintf("%s-%d-%s", name, revision, uuid)), nil } + +// RetryProvisioning marks a provisioning error as transient on the machines. +func (c *Client) RetryProvisioning(p params.Entities) (params.ErrorResults, error) { + entityStatus := make([]params.EntityStatus, len(p.Entities)) + for i, entity := range p.Entities { + entityStatus[i] = params.EntityStatus{Tag: entity.Tag, Data: params.StatusData{"transient": true}} + } + return c.api.statusSetter.UpdateStatus(params.SetStatus{ + Entities: entityStatus, + }) +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/client/client_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/client/client_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/client/client_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/client/client_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -44,7 +44,7 @@ s.setUpScenario(c) status, err := s.APIState.Client().Status(nil) c.Assert(err, gc.IsNil) - c.Assert(status, gc.DeepEquals, scenarioStatus) + c.Assert(status, jc.DeepEquals, scenarioStatus) } func (s *clientSuite) TestCompatibleSettingsParsing(c *gc.C) { @@ -220,7 +220,12 @@ { about: "invalid URL", url: "not-valid", - err: `charm URL has invalid schema: "not-valid"`, + err: "charm url series is not resolved", + }, + { + about: "invalid schema", + url: "not-valid:your-arguments", + err: `charm URL has invalid schema: "not-valid:your-arguments"`, }, { about: "unknown charm", @@ -660,9 +665,8 @@ _, restore := makeMockCharmStore() defer restore() for url, expect := range map[string]string{ - // TODO(fwereade) make these errors consistent one day. - "wordpress": `charm URL has invalid schema: "wordpress"`, - "cs:wordpress": `charm URL without series: "cs:wordpress"`, + "wordpress": "charm url series is not resolved", + "cs:wordpress": "charm url series is not resolved", "cs:precise/wordpress": "charm url must include revision", "cs:precise/wordpress-999999": `cannot download charm ".*": charm not found in mock store: cs:precise/wordpress-999999`, } { @@ -676,29 +680,36 @@ } } -func (s *clientSuite) TestClientServiceDeployPrincipal(c *gc.C) { - // TODO(fwereade): test ToMachineSpec directly on srvClient, when we - // manage to extract it as a package and can thus do it conveniently. +func (s *clientSuite) TestClientServiceDeployWithNetworks(c *gc.C) { store, restore := makeMockCharmStore() defer restore() curl, bundle := addCharm(c, store, "dummy") mem4g := constraints.MustParse("mem=4G") - err := s.APIState.Client().ServiceDeploy( - curl.String(), "service", 3, "", mem4g, "", + err := s.APIState.Client().ServiceDeployWithNetworks( + curl.String(), "service", 3, "", mem4g, "", []string{"net1", "net2"}, []string{"net3"}, ) c.Assert(err, gc.IsNil) - service, err := s.State.Service("service") + service := s.assertPrincipalDeployed(c, "service", curl, false, bundle, mem4g) + + include, exclude, err := service.Networks() + c.Assert(err, gc.IsNil) + c.Assert(include, gc.DeepEquals, []string{"net1", "net2"}) + c.Assert(exclude, gc.DeepEquals, []string{"net3"}) +} + +func (s *clientSuite) assertPrincipalDeployed(c *gc.C, serviceName string, curl *charm.URL, forced bool, bundle charm.Charm, cons constraints.Value) *state.Service { + service, err := s.State.Service(serviceName) c.Assert(err, gc.IsNil) charm, force, err := service.Charm() c.Assert(err, gc.IsNil) - c.Assert(force, gc.Equals, false) + c.Assert(force, gc.Equals, forced) c.Assert(charm.URL(), gc.DeepEquals, curl) c.Assert(charm.Meta(), gc.DeepEquals, bundle.Meta()) c.Assert(charm.Config(), gc.DeepEquals, bundle.Config()) - cons, err := service.Constraints() + serviceCons, err := service.Constraints() c.Assert(err, gc.IsNil) - c.Assert(cons, gc.DeepEquals, mem4g) + c.Assert(serviceCons, gc.DeepEquals, cons) units, err := service.AllUnits() c.Assert(err, gc.IsNil) for _, unit := range units { @@ -706,10 +717,25 @@ c.Assert(err, gc.IsNil) machine, err := s.State.Machine(mid) c.Assert(err, gc.IsNil) - cons, err := machine.Constraints() + machineCons, err := machine.Constraints() c.Assert(err, gc.IsNil) - c.Assert(cons, gc.DeepEquals, mem4g) + c.Assert(machineCons, gc.DeepEquals, cons) } + return service +} + +func (s *clientSuite) TestClientServiceDeployPrincipal(c *gc.C) { + // TODO(fwereade): test ToMachineSpec directly on srvClient, when we + // manage to extract it as a package and can thus do it conveniently. + store, restore := makeMockCharmStore() + defer restore() + curl, bundle := addCharm(c, store, "dummy") + mem4g := constraints.MustParse("mem=4G") + err := s.APIState.Client().ServiceDeploy( + curl.String(), "service", 3, "", mem4g, "", + ) + c.Assert(err, gc.IsNil) + s.assertPrincipalDeployed(c, "service", curl, false, bundle, mem4g) } func (s *clientSuite) TestClientServiceDeploySubordinate(c *gc.C) { @@ -838,9 +864,8 @@ defer restore() s.AddTestingService(c, "wordpress", s.AddTestingCharm(c, "wordpress")) for charmUrl, expect := range map[string]string{ - // TODO(fwereade,Makyo) make these errors consistent one day. - "wordpress": `charm URL has invalid schema: "wordpress"`, - "cs:wordpress": `charm URL without series: "cs:wordpress"`, + "wordpress": "charm url series is not resolved", + "cs:wordpress": "charm url series is not resolved", "cs:precise/wordpress": "charm url must include revision", "cs:precise/wordpress-999999": `cannot download charm ".*": charm not found in mock store: cs:precise/wordpress-999999`, } { @@ -1073,8 +1098,8 @@ s.AddTestingService(c, "wordpress", s.AddTestingCharm(c, "wordpress")) for url, expect := range map[string]string{ // TODO(fwereade,Makyo) make these errors consistent one day. - "wordpress": `charm URL has invalid schema: "wordpress"`, - "cs:wordpress": `charm URL without series: "cs:wordpress"`, + "wordpress": "charm url series is not resolved", + "cs:wordpress": "charm url series is not resolved", "cs:precise/wordpress": "charm url must include revision", "cs:precise/wordpress-999999": `cannot download charm ".*": charm not found in mock store: cs:precise/wordpress-999999`, } { @@ -1273,9 +1298,14 @@ c.Assert(err, gc.IsNil) if !c.Check(deltas, gc.DeepEquals, []params.Delta{{ Entity: ¶ms.MachineInfo{ - Id: m.Id(), - InstanceId: "i-0", - Status: params.StatusPending, + Id: m.Id(), + InstanceId: "i-0", + Status: params.StatusPending, + Life: params.Alive, + Series: "quantal", + Jobs: []params.MachineJob{state.JobManageEnviron.ToParams()}, + Addresses: []instance.Address{}, + HardwareCharacteristics: &instance.HardwareCharacteristics{}, }, }}) { c.Logf("got:") @@ -1534,6 +1564,47 @@ c.Assert(err, gc.IsNil) } +func (s *clientSuite) TestClientEnvironmentUnset(c *gc.C) { + err := s.State.UpdateEnvironConfig(map[string]interface{}{"abc": 123}, nil, nil) + c.Assert(err, gc.IsNil) + envConfig, err := s.State.EnvironConfig() + c.Assert(err, gc.IsNil) + _, found := envConfig.AllAttrs()["abc"] + c.Assert(found, jc.IsTrue) + + err = s.APIState.Client().EnvironmentUnset("abc") + c.Assert(err, gc.IsNil) + envConfig, err = s.State.EnvironConfig() + c.Assert(err, gc.IsNil) + _, found = envConfig.AllAttrs()["abc"] + c.Assert(found, jc.IsFalse) +} + +func (s *clientSuite) TestClientEnvironmentUnsetMissing(c *gc.C) { + // It's okay to unset a non-existent attribute. + err := s.APIState.Client().EnvironmentUnset("not_there") + c.Assert(err, gc.IsNil) +} + +func (s *clientSuite) TestClientEnvironmentUnsetError(c *gc.C) { + err := s.State.UpdateEnvironConfig(map[string]interface{}{"abc": 123}, nil, nil) + c.Assert(err, gc.IsNil) + envConfig, err := s.State.EnvironConfig() + c.Assert(err, gc.IsNil) + _, found := envConfig.AllAttrs()["abc"] + c.Assert(found, jc.IsTrue) + + // "type" may not be removed, and this will cause an error. + // If any one attribute's removal causes an error, there + // should be no change. + err = s.APIState.Client().EnvironmentUnset("abc", "type") + c.Assert(err, gc.ErrorMatches, "type: expected string, got nothing") + envConfig, err = s.State.EnvironConfig() + c.Assert(err, gc.IsNil) + _, found = envConfig.AllAttrs()["abc"] + c.Assert(found, jc.IsTrue) +} + func (s *clientSuite) TestClientFindTools(c *gc.C) { result, err := s.APIState.Client().FindTools(2, -1, "", "") c.Assert(err, gc.IsNil) @@ -1839,8 +1910,8 @@ client := s.APIState.Client() // First test the sanity checks. - err := client.AddCharm(&charm.URL{Name: "nonsense"}) - c.Assert(err, gc.ErrorMatches, `charm URL has invalid schema: ":/nonsense-0"`) + err := client.AddCharm(&charm.URL{Reference: charm.Reference{Name: "nonsense"}}) + c.Assert(err, gc.ErrorMatches, `charm URL has invalid schema: ":nonsense-0"`) err = client.AddCharm(charm.MustParseURL("local:precise/dummy")) c.Assert(err, gc.ErrorMatches, "only charm store charm URLs are supported, with cs: schema") err = client.AddCharm(charm.MustParseURL("cs:precise/wordpress")) @@ -1996,3 +2067,18 @@ func getArchiveName(bundleURL *url.URL) string { return strings.TrimPrefix(bundleURL.RequestURI(), "/dummyenv/private/") } + +func (s *clientSuite) TestRetryProvisioning(c *gc.C) { + machine, err := s.State.AddMachine("quantal", state.JobHostUnits) + c.Assert(err, gc.IsNil) + err = machine.SetStatus(params.StatusError, "error", nil) + c.Assert(err, gc.IsNil) + _, err = s.APIState.Client().RetryProvisioning(machine.Tag()) + c.Assert(err, gc.IsNil) + + status, info, data, err := machine.Status() + c.Assert(err, gc.IsNil) + c.Assert(status, gc.Equals, params.StatusError) + c.Assert(info, gc.Equals, "error") + c.Assert(data["transient"], gc.Equals, true) +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/client/perm_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/client/perm_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/client/perm_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/client/perm_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -69,6 +69,10 @@ op: opClientServiceDeploy, allow: []string{"user-admin", "user-other"}, }, { + about: "Client.ServiceDeployWithNetworks", + op: opClientServiceDeployWithNetworks, + allow: []string{"user-admin", "user-other"}, +}, { about: "Client.ServiceUpdate", op: opClientServiceUpdate, allow: []string{"user-admin", "user-other"}, @@ -215,7 +219,7 @@ c.Check(status, gc.IsNil) return func() {}, err } - c.Assert(status, gc.DeepEquals, scenarioStatus) + c.Assert(status, jc.DeepEquals, scenarioStatus) return func() {}, nil } @@ -318,6 +322,14 @@ if err.Error() == `charm URL has invalid schema: "mad:bad/url-1"` { err = nil } + return func() {}, err +} + +func opClientServiceDeployWithNetworks(c *gc.C, st *api.State, mst *state.State) (func(), error) { + err := st.Client().ServiceDeployWithNetworks("mad:bad/url-1", "x", 1, "", constraints.Value{}, "", nil, nil) + if err.Error() == `charm URL has invalid schema: "mad:bad/url-1"` { + err = nil + } return func() {}, err } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/client/run_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/client/run_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/client/run_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/client/run_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -76,16 +76,16 @@ func (s *runSuite) TestGetAllUnitNames(c *gc.C) { charm := s.AddTestingCharm(c, "dummy") - magic, err := s.State.AddService("magic", "user-admin", charm) + magic, err := s.State.AddService("magic", "user-admin", charm, nil, nil) s.addUnit(c, magic) s.addUnit(c, magic) - notAssigned, err := s.State.AddService("not-assigned", "user-admin", charm) + notAssigned, err := s.State.AddService("not-assigned", "user-admin", charm, nil, nil) c.Assert(err, gc.IsNil) _, err = notAssigned.AddUnit() c.Assert(err, gc.IsNil) - _, err = s.State.AddService("no-units", "user-admin", charm) + _, err = s.State.AddService("no-units", "user-admin", charm, nil, nil) c.Assert(err, gc.IsNil) for i, test := range []struct { @@ -241,7 +241,7 @@ s.addMachineWithAddress(c, "10.3.2.1") charm := s.AddTestingCharm(c, "dummy") - magic, err := s.State.AddService("magic", "user-admin", charm) + magic, err := s.State.AddService("magic", "user-admin", charm, nil, nil) s.addUnit(c, magic) s.addUnit(c, magic) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/addresses.go juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/common/addresses.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/addresses.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/common/addresses.go 2014-03-27 15:48:42.000000000 +0000 @@ -4,7 +4,10 @@ package common import ( + "launchpad.net/juju-core/instance" + "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api/params" + "launchpad.net/juju-core/state/watcher" ) // AddressAndCertGetter can be used to find out @@ -13,21 +16,50 @@ Addresses() ([]string, error) APIAddressesFromMachines() ([]string, error) CACert() []byte + APIHostPorts() ([][]instance.HostPort, error) + WatchAPIHostPorts() state.NotifyWatcher } // APIAddresser implements the APIAddresses method type APIAddresser struct { - getter AddressAndCertGetter + resources *Resources + getter AddressAndCertGetter } // NewAPIAddresser returns a new APIAddresser that uses the given getter to // fetch its addresses. -func NewAPIAddresser(getter AddressAndCertGetter) *APIAddresser { - return &APIAddresser{getter} +func NewAPIAddresser(getter AddressAndCertGetter, resources *Resources) *APIAddresser { + return &APIAddresser{ + getter: getter, + resources: resources, + } +} + +// APIHostPorts returns the API server addresses. +func (api *APIAddresser) APIHostPorts() (params.APIHostPortsResult, error) { + servers, err := api.getter.APIHostPorts() + if err != nil { + return params.APIHostPortsResult{}, err + } + return params.APIHostPortsResult{ + Servers: servers, + }, nil +} + +// WatchAPIHostPorts watches the API server addresses. +func (api *APIAddresser) WatchAPIHostPorts() (params.NotifyWatchResult, error) { + watch := api.getter.WatchAPIHostPorts() + if _, ok := <-watch.Changes(); ok { + return params.NotifyWatchResult{ + NotifyWatcherId: api.resources.Register(watch), + }, nil + } + return params.NotifyWatchResult{}, watcher.MustErr(watch) } // APIAddresses returns the list of addresses used to connect to the API. func (a *APIAddresser) APIAddresses() (params.StringsResult, error) { + // TODO(rog) change this to use api.st.APIHostPorts() addrs, err := a.getter.APIAddressesFromMachines() if err != nil { return params.StringsResult{}, err @@ -37,6 +69,13 @@ }, nil } +// CACert returns the certificate used to validate the state connection. +func (a *APIAddresser) CACert() params.BytesResult { + return params.BytesResult{ + Result: a.getter.CACert(), + } +} + // StateAddresser implements a common set of methods for getting state // server addresses, and the CA certificate used to authenticate them. type StateAddresser struct { @@ -59,14 +98,3 @@ Result: addrs, }, nil } - -// CACert returns the certificate used to validate the state connection. -// Note: there is an open bug that Uniter (which uses only APIAddresser) should -// add CACert to its interface. When it does, this API si likely to move to -// APIAddresser instead of StateAddresser. (All other users of StateAddresser -// already also expose APIAddresser) -func (a *StateAddresser) CACert() params.BytesResult { - return params.BytesResult{ - Result: a.getter.CACert(), - } -} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/addresses_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/common/addresses_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/addresses_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/common/addresses_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -6,6 +6,7 @@ import ( gc "launchpad.net/gocheck" + "launchpad.net/juju-core/instance" "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/apiserver/common" ) @@ -34,13 +35,8 @@ c.Assert(result.Result, gc.DeepEquals, []string{"addresses:1", "addresses:2"}) } -func (s *stateAddresserSuite) TestCACert(c *gc.C) { - result := s.addresser.CACert() - c.Assert(string(result.Result), gc.Equals, "a cert") -} - func (s *apiAddresserSuite) SetUpTest(c *gc.C) { - s.addresser = common.NewAPIAddresser(fakeAddresses{}) + s.addresser = common.NewAPIAddresser(fakeAddresses{}, common.NewResources()) } func (s *apiAddresserSuite) TestAPIAddresses(c *gc.C) { @@ -49,6 +45,13 @@ c.Assert(result.Result, gc.DeepEquals, []string{"apiaddresses:1", "apiaddresses:2"}) } +func (s *apiAddresserSuite) TestCACert(c *gc.C) { + result := s.addresser.CACert() + c.Assert(string(result.Result), gc.Equals, "a cert") +} + +var _ common.AddressAndCertGetter = fakeAddresses{} + type fakeAddresses struct{} func (fakeAddresses) Addresses() ([]string, error) { @@ -62,3 +65,11 @@ func (fakeAddresses) CACert() []byte { return []byte("a cert") } + +func (fakeAddresses) APIHostPorts() ([][]instance.HostPort, error) { + panic("should never be called") +} + +func (fakeAddresses) WatchAPIHostPorts() state.NotifyWatcher { + panic("should never be called") +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/environwatcher.go juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/common/environwatcher.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/environwatcher.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/common/environwatcher.go 2014-03-27 15:48:42.000000000 +0000 @@ -36,6 +36,9 @@ // WatchForEnvironConfigChanges returns a NotifyWatcher that observes // changes to the environment configuration. +// Note that although the NotifyWatchResult contains an Error field, +// it's not used because we are only returning a single watcher, +// so we use the regular error return. func (e *EnvironWatcher) WatchForEnvironConfigChanges() (params.NotifyWatchResult, error) { result := params.NotifyWatchResult{} @@ -46,7 +49,6 @@ // TODO(dimitern) If we have multiple environments in state, use a // tag argument here and as a method argument. if !canWatch("") { - result.Error = ServerError(ErrPerm) return result, ErrPerm } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/environwatcher_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/common/environwatcher_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/environwatcher_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/common/environwatcher_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -16,7 +16,6 @@ "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/apiserver/common" - apiservertesting "launchpad.net/juju-core/state/apiserver/testing" "launchpad.net/juju-core/testing" "launchpad.net/juju-core/testing/testbase" ) @@ -103,7 +102,7 @@ ) result, err := e.WatchForEnvironConfigChanges() c.Assert(err, gc.ErrorMatches, "permission denied") - c.Assert(result.Error, jc.DeepEquals, apiservertesting.ErrUnauthorized) + c.Assert(result, gc.DeepEquals, params.NotifyWatchResult{}) c.Assert(resources.Count(), gc.Equals, 0) } @@ -123,7 +122,6 @@ ) result, err := e.EnvironConfig() c.Assert(err, gc.IsNil) - c.Assert(result.Error, gc.IsNil) // Make sure we can read the secret attribute (i.e. it's not masked). c.Check(result.Config["secret"], gc.Equals, "pork") c.Check(map[string]interface{}(result.Config), jc.DeepEquals, testingEnvConfig.AllAttrs()) @@ -177,7 +175,6 @@ ) result, err := e.EnvironConfig() c.Assert(err, gc.IsNil) - c.Assert(result.Error, gc.IsNil) // Make sure the secret attribute is masked. c.Check(result.Config["secret"], gc.Equals, "not available") // And only that is masked. diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/setstatus.go juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/common/setstatus.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/setstatus.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/common/setstatus.go 2014-03-27 15:48:42.000000000 +0000 @@ -4,6 +4,8 @@ package common import ( + "fmt" + "launchpad.net/juju-core/state" "launchpad.net/juju-core/state/api/params" ) @@ -56,5 +58,58 @@ } result.Results[i].Error = ServerError(err) } + return result, nil +} + +func (s *StatusSetter) updateEntityStatusData(tag string, data params.StatusData) error { + entity0, err := s.st.FindEntity(tag) + if err != nil { + return err + } + statusGetter, ok := entity0.(state.StatusGetter) + if !ok { + return NotSupportedError(tag, "getting status") + } + existingStatus, existingInfo, existingData, err := statusGetter.Status() + if err != nil { + return err + } + newData := existingData + if newData == nil { + newData = data + } else { + for k, v := range data { + newData[k] = v + } + } + entity, ok := entity0.(state.StatusSetter) + if !ok { + return NotSupportedError(tag, "updating status") + } + if len(newData) > 0 && existingStatus != params.StatusError { + return fmt.Errorf("machine %q is not in an error state", tag) + } + return entity.SetStatus(existingStatus, existingInfo, newData) +} + +// UpdateStatus updates the status data of each given entity. +func (s *StatusSetter) UpdateStatus(args params.SetStatus) (params.ErrorResults, error) { + result := params.ErrorResults{ + Results: make([]params.ErrorResult, len(args.Entities)), + } + if len(args.Entities) == 0 { + return result, nil + } + canModify, err := s.getCanModify() + if err != nil { + return params.ErrorResults{}, err + } + for i, arg := range args.Entities { + err := ErrPerm + if canModify(arg.Tag) { + err = s.updateEntityStatusData(arg.Tag, arg.Data) + } + result.Results[i].Error = ServerError(err) + } return result, nil } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/setstatus_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/common/setstatus_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/setstatus_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/common/setstatus_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -34,6 +34,17 @@ return s.err } +func (s *fakeStatusSetter) Status() (status params.Status, info string, data params.StatusData, err error) { + return s.status, s.info, s.data, nil +} + +func (s *fakeStatusSetter) UpdateStatus(data params.StatusData) error { + for k, v := range data { + s.data[k] = v + } + return s.err +} + func (*statusSetterSuite) TestSetStatus(c *gc.C) { st := &fakeState{ entities: map[string]entityWithError{ @@ -55,7 +66,7 @@ } s := common.NewStatusSetter(st, getCanModify) args := params.SetStatus{ - Entities: []params.SetEntityStatus{ + Entities: []params.EntityStatus{ {"x0", params.StatusStarted, "bar", nil}, {"x1", params.StatusStopped, "", nil}, {"x2", params.StatusPending, "not really", nil}, @@ -91,7 +102,7 @@ } s := common.NewStatusSetter(&fakeState{}, getCanModify) args := params.SetStatus{ - Entities: []params.SetEntityStatus{{"x0", "", "", nil}}, + Entities: []params.EntityStatus{{"x0", "", "", nil}}, } _, err := s.SetStatus(args) c.Assert(err, gc.ErrorMatches, "pow") @@ -106,3 +117,59 @@ c.Assert(err, gc.IsNil) c.Assert(result.Results, gc.HasLen, 0) } + +func (*statusSetterSuite) TestUpdateStatus(c *gc.C) { + st := &fakeState{ + entities: map[string]entityWithError{ + "x0": &fakeStatusSetter{status: params.StatusPending, info: "blah", err: fmt.Errorf("x0 fails")}, + "x1": &fakeStatusSetter{status: params.StatusError, info: "foo", data: params.StatusData{"foo": "blah"}}, + "x2": &fakeStatusSetter{status: params.StatusError, info: "some info"}, + "x3": &fakeStatusSetter{fetchError: "x3 error"}, + "x4": &fakeStatusSetter{status: params.StatusStarted}, + "x5": &fakeStatusSetter{status: params.StatusStopped, info: ""}, + }, + } + getCanModify := func() (common.AuthFunc, error) { + return func(tag string) bool { + switch tag { + case "x0", "x1", "x2", "x3", "x4": + return true + } + return false + }, nil + } + s := common.NewStatusSetter(st, getCanModify) + args := params.SetStatus{ + Entities: []params.EntityStatus{ + {Tag: "x0", Data: nil}, + {Tag: "x1", Data: nil}, + {Tag: "x2", Data: params.StatusData{"foo": "bar"}}, + {Tag: "x3", Data: params.StatusData{"foo": "bar"}}, + {Tag: "x4", Data: params.StatusData{"foo": "bar"}}, + {Tag: "x5", Data: params.StatusData{"foo": "bar"}}, + {Tag: "x6", Data: nil}, + }, + } + result, err := s.UpdateStatus(args) + c.Assert(err, gc.IsNil) + c.Assert(result, gc.DeepEquals, params.ErrorResults{ + Results: []params.ErrorResult{ + {¶ms.Error{Message: "x0 fails"}}, + {nil}, + {nil}, + {¶ms.Error{Message: "x3 error"}}, + {¶ms.Error{Message: `machine "x4" is not in an error state`}}, + {apiservertesting.ErrUnauthorized}, + {apiservertesting.ErrUnauthorized}, + }, + }) + get := func(tag string) *fakeStatusSetter { + return st.entities[tag].(*fakeStatusSetter) + } + c.Assert(get("x1").status, gc.Equals, params.StatusError) + c.Assert(get("x1").info, gc.Equals, "foo") + c.Assert(get("x1").data, gc.DeepEquals, params.StatusData{"foo": "blah"}) + c.Assert(get("x2").status, gc.Equals, params.StatusError) + c.Assert(get("x2").info, gc.Equals, "some info") + c.Assert(get("x2").data, gc.DeepEquals, params.StatusData{"foo": "bar"}) +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/testing/environwatcher.go juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/common/testing/environwatcher.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/common/testing/environwatcher.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/common/testing/environwatcher.go 2014-03-27 15:48:42.000000000 +0000 @@ -48,7 +48,6 @@ result, err := envWatcher.EnvironConfig() c.Assert(err, gc.IsNil) - c.Assert(result.Error, gc.IsNil) configAttributes := envConfig.AllAttrs() // If the implementor doesn't provide secrets, we need to replace the config diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/deployer/deployer.go juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/deployer/deployer.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/deployer/deployer.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/deployer/deployer.go 2014-03-27 15:48:42.000000000 +0000 @@ -60,7 +60,7 @@ PasswordChanger: common.NewPasswordChanger(st, getAuthFunc), LifeGetter: common.NewLifeGetter(st, getAuthFunc), StateAddresser: common.NewStateAddresser(st), - APIAddresser: common.NewAPIAddresser(st), + APIAddresser: common.NewAPIAddresser(st, resources), UnitsWatcher: common.NewUnitsWatcher(st, resources, getCanWatch), st: st, resources: resources, diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/machine/machiner.go juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/machine/machiner.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/machine/machiner.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/machine/machiner.go 2014-03-27 15:48:42.000000000 +0000 @@ -18,6 +18,7 @@ *common.StatusSetter *common.DeadEnsurer *common.AgentEntityWatcher + *common.APIAddresser st *state.State auth common.Authorizer @@ -41,6 +42,7 @@ StatusSetter: common.NewStatusSetter(st, getCanModify), DeadEnsurer: common.NewDeadEnsurer(st, getCanModify), AgentEntityWatcher: common.NewAgentEntityWatcher(st, resources, getCanRead), + APIAddresser: common.NewAPIAddresser(st, resources), st: st, auth: authorizer, getCanModify: getCanModify, diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/machine/machiner_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/machine/machiner_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/machine/machiner_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/machine/machiner_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -57,7 +57,7 @@ c.Assert(err, gc.IsNil) args := params.SetStatus{ - Entities: []params.SetEntityStatus{ + Entities: []params.EntityStatus{ {Tag: "machine-1", Status: params.StatusError, Info: "not really"}, {Tag: "machine-0", Status: params.StatusStopped, Info: "foobar"}, {Tag: "machine-42", Status: params.StatusStarted, Info: "blah"}, diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/provisioner/machineerror.go juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/provisioner/machineerror.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/provisioner/machineerror.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/provisioner/machineerror.go 2014-03-27 15:48:42.000000000 +0000 @@ -0,0 +1,80 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package provisioner + +import ( + "time" + + "launchpad.net/tomb" + + "launchpad.net/juju-core/state" +) + +// machineErrorRetry is a notify watcher that fires when it is +// appropriate to retry provisioning machines with transient errors. +type machineErrorRetry struct { + tomb tomb.Tomb + out chan struct{} +} + +func newWatchMachineErrorRetry() state.NotifyWatcher { + w := &machineErrorRetry{ + out: make(chan struct{}), + } + go func() { + defer w.tomb.Done() + defer close(w.out) + w.tomb.Kill(w.loop()) + }() + return w +} + +// Stop stops the watcher, and returns any error encountered while running +// or shutting down. +func (w *machineErrorRetry) Stop() error { + w.Kill() + return w.Wait() +} + +// Kill kills the watcher without waiting for it to shut down. +func (w *machineErrorRetry) Kill() { + w.tomb.Kill(nil) +} + +// Wait waits for the watcher to die and returns any +// error encountered when it was running. +func (w *machineErrorRetry) Wait() error { + return w.tomb.Wait() +} + +// Err returns any error encountered while running or shutting down, or +// tomb.ErrStillAlive if the watcher is still running. +func (w *machineErrorRetry) Err() error { + return w.tomb.Err() +} + +// Changes returns the event channel for the machineErrorRetry watcher. +func (w *machineErrorRetry) Changes() <-chan struct{} { + return w.out +} + +// ErrorRetryWaitDelay is the poll time currently used to trigger the watcher. +var ErrorRetryWaitDelay = 1 * time.Minute + +// The initial implementation of this watcher simply acts as a poller, +// triggering every ErrorRetryWaitDelay minutes. +func (w *machineErrorRetry) loop() error { + out := w.out + for { + select { + case <-w.tomb.Dying(): + return tomb.ErrDying + case <-time.After(ErrorRetryWaitDelay): + out = w.out + case out <- struct{}{}: + out = nil + } + } + return nil +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/provisioner/provisioner.go juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/provisioner/provisioner.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/provisioner/provisioner.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/provisioner/provisioner.go 2014-03-27 15:48:42.000000000 +0000 @@ -27,10 +27,11 @@ *common.EnvironMachinesWatcher *common.InstanceIdGetter - st *state.State - resources *common.Resources - authorizer common.Authorizer - getAuthFunc common.GetAuthFunc + st *state.State + resources *common.Resources + authorizer common.Authorizer + getAuthFunc common.GetAuthFunc + getCanWatchMachines common.GetAuthFunc } // NewProvisionerAPI creates a new server-side ProvisionerAPI facade. @@ -78,7 +79,7 @@ PasswordChanger: common.NewPasswordChanger(st, getAuthFunc), LifeGetter: common.NewLifeGetter(st, getAuthFunc), StateAddresser: common.NewStateAddresser(st), - APIAddresser: common.NewAPIAddresser(st), + APIAddresser: common.NewAPIAddresser(st, resources), ToolsGetter: common.NewToolsGetter(st, getAuthFunc), EnvironWatcher: common.NewEnvironWatcher(st, resources, getCanWatch, getCanReadSecrets), EnvironMachinesWatcher: common.NewEnvironMachinesWatcher(st, resources, getCanReadSecrets), @@ -87,6 +88,7 @@ resources: resources, authorizer: authorizer, getAuthFunc: getAuthFunc, + getCanWatchMachines: getCanReadSecrets, }, nil } @@ -214,13 +216,53 @@ machine, err := p.getMachine(canAccess, entity.Tag) if err == nil { r := &result.Results[i] - r.Status, r.Info, _, err = machine.Status() + r.Status, r.Info, r.Data, err = machine.Status() } result.Results[i].Error = common.ServerError(err) } return result, nil } +// MachinesWithTransientErrors returns status data for machines with provisioning +// errors which are transient. +func (p *ProvisionerAPI) MachinesWithTransientErrors() (params.StatusResults, error) { + results := params.StatusResults{} + canAccessFunc, err := p.getAuthFunc() + if err != nil { + return results, err + } + // TODO (wallyworld) - add state.State API for more efficient machines query + machines, err := p.st.AllMachines() + if err != nil { + return results, err + } + for _, machine := range machines { + if !canAccessFunc(machine.Tag()) { + continue + } + if _, provisionedErr := machine.InstanceId(); provisionedErr == nil { + // Machine may have been provisioned but machiner hasn't set the + // status to Started yet. + continue + } + result := params.StatusResult{} + if result.Status, result.Info, result.Data, err = machine.Status(); err != nil { + continue + } + if result.Status != params.StatusError { + continue + } + // Transient errors are marked as such in the status data. + if transient, ok := result.Data["transient"].(bool); !ok || !transient { + continue + } + result.Id = machine.Id() + result.Life = params.Life(machine.Life().String()) + results.Results = append(results.Results, result) + } + return results, nil +} + // Series returns the deployed series for each given machine entity. func (p *ProvisionerAPI) Series(args params.Entities) (params.StringResults, error) { result := params.StringResults{ @@ -283,3 +325,24 @@ } return result, nil } + +// WatchMachineErrorRetry returns a NotifyWatcher that notifies when +// the provisioner should retry provisioning machines with transient errors. +func (p *ProvisionerAPI) WatchMachineErrorRetry() (params.NotifyWatchResult, error) { + result := params.NotifyWatchResult{} + canWatch, err := p.getCanWatchMachines() + if err != nil { + return params.NotifyWatchResult{}, err + } + if !canWatch("") { + return result, common.ErrPerm + } + watch := newWatchMachineErrorRetry() + // Consume any initial event and forward it to the result. + if _, ok := <-watch.Changes(); ok { + result.NotifyWatcherId = p.resources.Register(watch) + } else { + return result, watcher.MustErr(watch) + } + return result, nil +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/provisioner/provisioner_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/provisioner/provisioner_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/provisioner/provisioner_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/provisioner/provisioner_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -57,7 +57,7 @@ if withStateServer { s.machines = append(s.machines, testing.AddStateServerMachine(c, s.State)) } - for i := 0; i < 3; i++ { + for i := 0; i < 5; i++ { machine, err := s.State.AddMachine("quantal", state.JobHostUnits) c.Check(err, gc.IsNil) s.machines = append(s.machines, machine) @@ -119,6 +119,8 @@ {Tag: s.machines[0].Tag(), Password: "xxx0-1234567890123457890"}, {Tag: s.machines[1].Tag(), Password: "xxx1-1234567890123457890"}, {Tag: s.machines[2].Tag(), Password: "xxx2-1234567890123457890"}, + {Tag: s.machines[3].Tag(), Password: "xxx3-1234567890123457890"}, + {Tag: s.machines[4].Tag(), Password: "xxx4-1234567890123457890"}, {Tag: "machine-42", Password: "foo"}, {Tag: "unit-foo-0", Password: "zzz"}, {Tag: "service-bar", Password: "abc"}, @@ -131,6 +133,8 @@ {nil}, {nil}, {nil}, + {nil}, + {nil}, {apiservertesting.NotFoundError("machine 42")}, {apiservertesting.ErrUnauthorized}, {apiservertesting.ErrUnauthorized}, @@ -317,8 +321,9 @@ c.Assert(err, gc.IsNil) args := params.SetStatus{ - Entities: []params.SetEntityStatus{ - {Tag: s.machines[0].Tag(), Status: params.StatusError, Info: "not really"}, + Entities: []params.EntityStatus{ + {Tag: s.machines[0].Tag(), Status: params.StatusError, Info: "not really", + Data: params.StatusData{"foo": "bar"}}, {Tag: s.machines[1].Tag(), Status: params.StatusStopped, Info: "foobar"}, {Tag: s.machines[2].Tag(), Status: params.StatusStarted, Info: "again"}, {Tag: "machine-42", Status: params.StatusStarted, Info: "blah"}, @@ -339,9 +344,65 @@ }) // Verify the changes. - s.assertStatus(c, 0, params.StatusError, "not really") - s.assertStatus(c, 1, params.StatusStopped, "foobar") - s.assertStatus(c, 2, params.StatusStarted, "again") + s.assertStatus(c, 0, params.StatusError, "not really", params.StatusData{"foo": "bar"}) + s.assertStatus(c, 1, params.StatusStopped, "foobar", params.StatusData{}) + s.assertStatus(c, 2, params.StatusStarted, "again", params.StatusData{}) +} + +func (s *withoutStateServerSuite) TestMachinesWithTransientErrors(c *gc.C) { + err := s.machines[0].SetStatus(params.StatusStarted, "blah", nil) + c.Assert(err, gc.IsNil) + err = s.machines[1].SetStatus(params.StatusError, "transient error", + params.StatusData{"transient": true, "foo": "bar"}) + c.Assert(err, gc.IsNil) + err = s.machines[2].SetStatus(params.StatusError, "error", params.StatusData{"transient": false}) + c.Assert(err, gc.IsNil) + err = s.machines[3].SetStatus(params.StatusError, "error", nil) + c.Assert(err, gc.IsNil) + // Machine 4 is provisioned but error not reset yet. + err = s.machines[4].SetStatus(params.StatusError, "transient error", + params.StatusData{"transient": true, "foo": "bar"}) + c.Assert(err, gc.IsNil) + hwChars := instance.MustParseHardware("arch=i386", "mem=4G") + err = s.machines[4].SetProvisioned("i-am", "fake_nonce", &hwChars) + c.Assert(err, gc.IsNil) + + result, err := s.provisioner.MachinesWithTransientErrors() + c.Assert(err, gc.IsNil) + c.Assert(result, gc.DeepEquals, params.StatusResults{ + Results: []params.StatusResult{ + {Id: "1", Life: "alive", Status: "error", Info: "transient error", + Data: params.StatusData{"transient": true, "foo": "bar"}}, + }, + }) +} + +func (s *withoutStateServerSuite) TestMachinesWithTransientErrorsPermission(c *gc.C) { + // Machines where there's permission issues are omitted. + anAuthorizer := s.authorizer + anAuthorizer.MachineAgent = true + anAuthorizer.EnvironManager = false + anAuthorizer.Tag = "machine-1" + aProvisioner, err := provisioner.NewProvisionerAPI(s.State, s.resources, + anAuthorizer) + err = s.machines[0].SetStatus(params.StatusStarted, "blah", nil) + c.Assert(err, gc.IsNil) + err = s.machines[1].SetStatus(params.StatusError, "transient error", + params.StatusData{"transient": true, "foo": "bar"}) + c.Assert(err, gc.IsNil) + err = s.machines[2].SetStatus(params.StatusError, "error", params.StatusData{"transient": false}) + c.Assert(err, gc.IsNil) + err = s.machines[3].SetStatus(params.StatusError, "error", nil) + c.Assert(err, gc.IsNil) + + result, err := aProvisioner.MachinesWithTransientErrors() + c.Assert(err, gc.IsNil) + c.Assert(result, gc.DeepEquals, params.StatusResults{ + Results: []params.StatusResult{ + {Id: "1", Life: "alive", Status: "error", Info: "transient error", + Data: params.StatusData{"transient": true, "foo": "bar"}}, + }, + }) } func (s *withoutStateServerSuite) TestEnsureDead(c *gc.C) { @@ -384,11 +445,14 @@ c.Assert(s.machines[index].Life(), gc.Equals, expectLife) } -func (s *withoutStateServerSuite) assertStatus(c *gc.C, index int, expectStatus params.Status, expectInfo string) { - status, info, _, err := s.machines[index].Status() +func (s *withoutStateServerSuite) assertStatus(c *gc.C, index int, expectStatus params.Status, expectInfo string, + expectData params.StatusData) { + + status, info, data, err := s.machines[index].Status() c.Assert(err, gc.IsNil) c.Assert(status, gc.Equals, expectStatus) c.Assert(info, gc.Equals, expectInfo) + c.Assert(data, gc.DeepEquals, expectData) } func (s *withoutStateServerSuite) TestWatchContainers(c *gc.C) { @@ -482,7 +546,7 @@ c.Assert(err, gc.IsNil) err = s.machines[1].SetStatus(params.StatusStopped, "foo", nil) c.Assert(err, gc.IsNil) - err = s.machines[2].SetStatus(params.StatusError, "not really", nil) + err = s.machines[2].SetStatus(params.StatusError, "not really", params.StatusData{"foo": "bar"}) c.Assert(err, gc.IsNil) args := params.Entities{Entities: []params.Entity{ @@ -497,9 +561,9 @@ c.Assert(err, gc.IsNil) c.Assert(result, gc.DeepEquals, params.StatusResults{ Results: []params.StatusResult{ - {Status: params.StatusStarted, Info: "blah"}, - {Status: params.StatusStopped, Info: "foo"}, - {Status: params.StatusError, Info: "not really"}, + {Status: params.StatusStarted, Info: "blah", Data: params.StatusData{}}, + {Status: params.StatusStopped, Info: "foo", Data: params.StatusData{}}, + {Status: params.StatusError, Info: "not really", Data: params.StatusData{"foo": "bar"}}, {Error: apiservertesting.NotFoundError("machine 42")}, {Error: apiservertesting.ErrUnauthorized}, {Error: apiservertesting.ErrUnauthorized}, @@ -650,7 +714,7 @@ c.Assert(err, gc.IsNil) c.Assert(result, gc.DeepEquals, params.StringsWatchResult{ StringsWatcherId: "1", - Changes: []string{"0", "1", "2"}, + Changes: []string{"0", "1", "2", "3", "4"}, }) // Verify the resources were registered and stop them when done. @@ -861,3 +925,35 @@ Result: s.State.CACert(), }) } + +func (s *withoutStateServerSuite) TestWatchMachineErrorRetry(c *gc.C) { + s.PatchValue(&provisioner.ErrorRetryWaitDelay, 2*coretesting.ShortWait) + c.Assert(s.resources.Count(), gc.Equals, 0) + + _, err := s.provisioner.WatchMachineErrorRetry() + c.Assert(err, gc.IsNil) + + // Verify the resources were registered and stop them when done. + c.Assert(s.resources.Count(), gc.Equals, 1) + resource := s.resources.Get("1") + defer statetesting.AssertStop(c, resource) + + // Check that the Watch has consumed the initial event ("returned" + // in the Watch call) + wc := statetesting.NewNotifyWatcherC(c, s.State, resource.(state.NotifyWatcher)) + wc.AssertNoChange() + + // We should now get a time triggered change. + wc.AssertOneChange() + + // Make sure WatchMachineErrorRetry fails with a machine agent login. + anAuthorizer := s.authorizer + anAuthorizer.MachineAgent = true + anAuthorizer.EnvironManager = false + aProvisioner, err := provisioner.NewProvisionerAPI(s.State, s.resources, anAuthorizer) + c.Assert(err, gc.IsNil) + + result, err := aProvisioner.WatchMachineErrorRetry() + c.Assert(err, gc.ErrorMatches, "permission denied") + c.Assert(result, gc.DeepEquals, params.NotifyWatchResult{}) +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/uniter/uniter.go juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/uniter/uniter.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/uniter/uniter.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/uniter/uniter.go 2014-03-27 15:48:42.000000000 +0000 @@ -60,7 +60,7 @@ StatusSetter: common.NewStatusSetter(st, accessUnit), DeadEnsurer: common.NewDeadEnsurer(st, accessUnit), AgentEntityWatcher: common.NewAgentEntityWatcher(st, resources, accessUnitOrService), - APIAddresser: common.NewAPIAddresser(st), + APIAddresser: common.NewAPIAddresser(st, resources), EnvironWatcher: common.NewEnvironWatcher(st, resources, getCanWatch, getCanReadSecrets), st: st, diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/uniter/uniter_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/uniter/uniter_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/apiserver/uniter/uniter_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/apiserver/uniter/uniter_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -109,7 +109,7 @@ c.Assert(err, gc.IsNil) args := params.SetStatus{ - Entities: []params.SetEntityStatus{ + Entities: []params.EntityStatus{ {Tag: "unit-mysql-0", Status: params.StatusError, Info: "not really"}, {Tag: "unit-wordpress-0", Status: params.StatusStopped, Info: "foobar"}, {Tag: "unit-foo-42", Status: params.StatusStarted, Info: "blah"}, diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/assign_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/assign_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/assign_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/assign_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -28,7 +28,13 @@ func (s *AssignSuite) SetUpTest(c *gc.C) { s.ConnSuite.SetUpTest(c) - wordpress := s.AddTestingService(c, "wordpress", s.AddTestingCharm(c, "wordpress")) + wordpress := s.AddTestingServiceWithNetworks( + c, + "wordpress", + s.AddTestingCharm(c, "wordpress"), + []string{"net1", "net2"}, + []string{"net3", "net4"}, + ) s.wordpress = wordpress } @@ -312,12 +318,21 @@ } func (s *AssignSuite) assertAssignedUnit(c *gc.C, unit *state.Unit) string { + // Get service networks. + service, err := unit.Service() + c.Assert(err, gc.IsNil) + includeNetworks, excludeNetworks, err := service.Networks() + c.Assert(err, gc.IsNil) // Check the machine on the unit is set. machineId, err := unit.AssignedMachineId() c.Assert(err, gc.IsNil) // Check that the principal is set on the machine. machine, err := s.State.Machine(machineId) c.Assert(err, gc.IsNil) + include, exclude, err := machine.Networks() + c.Assert(err, gc.IsNil) + c.Assert(include, gc.DeepEquals, includeNetworks) + c.Assert(exclude, gc.DeepEquals, excludeNetworks) machineUnits, err := machine.Units() c.Assert(err, gc.IsNil) c.Assert(machineUnits, gc.HasLen, 1) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/cleanup.go juju-core-1.17.7/src/launchpad.net/juju-core/state/cleanup.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/cleanup.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/cleanup.go 2014-03-27 15:48:42.000000000 +0000 @@ -154,6 +154,9 @@ if err := st.cleanupContainers(machine); err != nil { return err } + if err := st.cleanupNetworks(machineGlobalKey(machineId)); err != nil { + return err + } for _, unitName := range machine.doc.Principals { if err := st.obliterateUnit(unitName); err != nil { return err @@ -205,6 +208,16 @@ } return nil } + +// cleanupNetworks removes associated networks for a machine or +// service, given by its global key. +func (st *State) cleanupNetworks(globalKey string) error { + op := removeNetworksOp(st, globalKey) + if err := st.runTransaction([]txn.Op{op}); err != nil { + logger.Warningf("cannot remove networks document for %q: %v", globalKey, err) + } + return nil +} // obliterateUnit removes a unit from state completely. It is not safe or // sane to obliterate any unit in isolation; its only reasonable use is in diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/compat_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/compat_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/compat_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/compat_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -67,3 +67,40 @@ err = s.env.Destroy() c.Assert(err, gc.IsNil) } + +func (s *compatSuite) TestGetServiceWithoutNetworksIsOK(c *gc.C) { + _, err := s.state.AddUser(AdminUser, "pass") + c.Assert(err, gc.IsNil) + charm := addCharm(c, s.state, "quantal", testing.Charms.Dir("mysql")) + service, err := s.state.AddService("mysql", "user-admin", charm, nil, nil) + c.Assert(err, gc.IsNil) + // In 1.17.7+ all services have associated document in the + // networks collection. We remove it here to test backwards + // compatibility. + ops := []txn.Op{removeNetworksOp(s.state, service.globalKey())} + err = s.state.runTransaction(ops) + c.Assert(err, gc.IsNil) + + // Now check the trying to fetch service's networks is OK. + include, exclude, err := service.Networks() + c.Assert(err, gc.IsNil) + c.Assert(include, gc.HasLen, 0) + c.Assert(exclude, gc.HasLen, 0) +} + +func (s *compatSuite) TestGetMachineWithoutNetworksIsOK(c *gc.C) { + machine, err := s.state.AddMachine("quantal", JobHostUnits) + c.Assert(err, gc.IsNil) + // In 1.17.7+ all machines have associated document in the + // networks collection. We remove it here to test backwards + // compatibility. + ops := []txn.Op{removeNetworksOp(s.state, machine.globalKey())} + err = s.state.runTransaction(ops) + c.Assert(err, gc.IsNil) + + // Now check the trying to fetch machine's networks is OK. + include, exclude, err := machine.Networks() + c.Assert(err, gc.IsNil) + c.Assert(include, gc.HasLen, 0) + c.Assert(exclude, gc.HasLen, 0) +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/conn_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/conn_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/conn_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/conn_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -79,6 +79,10 @@ return state.AddTestingService(c, s.State, name, ch) } +func (s *ConnSuite) AddTestingServiceWithNetworks(c *gc.C, name string, ch *state.Charm, includeNetworks, excludeNetworks []string) *state.Service { + return state.AddTestingServiceWithNetworks(c, s.State, name, ch, includeNetworks, excludeNetworks) +} + func (s *ConnSuite) AddSeriesCharm(c *gc.C, name, series string) *state.Charm { return state.AddCustomCharm(c, s.State, name, "", "", series, -1) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/export_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/export_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/export_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/export_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -142,7 +142,11 @@ } func AddTestingService(c *gc.C, st *State, name string, ch *Charm) *Service { - service, err := st.AddService(name, "user-admin", ch) + return AddTestingServiceWithNetworks(c, st, name, ch, nil, nil) +} + +func AddTestingServiceWithNetworks(c *gc.C, st *State, name string, ch *Charm, includeNetworks, excludeNetworks []string) *Service { + service, err := st.AddService(name, "user-admin", ch, includeNetworks, excludeNetworks) c.Assert(err, gc.IsNil) return service } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/interface.go juju-core-1.17.7/src/launchpad.net/juju-core/state/interface.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/interface.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/interface.go 2014-03-27 15:48:42.000000000 +0000 @@ -37,9 +37,15 @@ SetStatus(status params.Status, info string, data params.StatusData) error } +type StatusGetter interface { + Status() (status params.Status, info string, data params.StatusData, err error) +} + var ( _ StatusSetter = (*Machine)(nil) _ StatusSetter = (*Unit)(nil) + _ StatusGetter = (*Machine)(nil) + _ StatusGetter = (*Unit)(nil) ) // Lifer represents an entity with a life. diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/machine.go juju-core-1.17.7/src/launchpad.net/juju-core/state/machine.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/machine.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/machine.go 2014-03-27 15:48:42.000000000 +0000 @@ -74,6 +74,15 @@ return -1, fmt.Errorf("invalid machine job %q", job) } +// paramsJobsFromJobs converts state jobs to params jobs. +func paramsJobsFromJobs(jobs []MachineJob) []params.MachineJob { + paramsJobs := make([]params.MachineJob, len(jobs)) + for i, machineJob := range jobs { + paramsJobs[i] = machineJob.ToParams() + } + return paramsJobs +} + func (job MachineJob) String() string { return string(job.ToParams()) } @@ -161,20 +170,24 @@ Tags *[]string `bson:"tags,omitempty"` } +func hardwareCharacteristics(instData instanceData) *instance.HardwareCharacteristics { + return &instance.HardwareCharacteristics{ + Arch: instData.Arch, + Mem: instData.Mem, + RootDisk: instData.RootDisk, + CpuCores: instData.CpuCores, + CpuPower: instData.CpuPower, + Tags: instData.Tags, + } +} + // TODO(wallyworld): move this method to a service. func (m *Machine) HardwareCharacteristics() (*instance.HardwareCharacteristics, error) { - hc := &instance.HardwareCharacteristics{} instData, err := getInstanceData(m.st, m.Id()) if err != nil { return nil, err } - hc.Arch = instData.Arch - hc.Mem = instData.Mem - hc.RootDisk = instData.RootDisk - hc.CpuCores = instData.CpuCores - hc.CpuPower = instData.CpuPower - hc.Tags = instData.Tags - return hc, nil + return hardwareCharacteristics(instData), nil } func getInstanceData(st *State, id string) (instanceData, error) { @@ -571,6 +584,7 @@ }, removeStatusOp(m.st, m.globalKey()), removeConstraintsOp(m.st, m.globalKey()), + removeNetworksOp(m.st, m.globalKey()), annotationRemoveOp(m.st, m.globalKey()), } ops = append(ops, removeContainerRefOps(m.st, m.Id())...) @@ -867,6 +881,12 @@ return nil } +// Networks returns the list of networks the machine should be on +// (includeNetworks) or not (excludeNetworks). +func (m *Machine) Networks() (includeNetworks, excludeNetworks []string, err error) { + return readNetworks(m.st, m.globalKey()) +} + // CheckProvisioned returns true if the machine was provisioned with the given nonce. func (m *Machine) CheckProvisioned(nonce string) bool { return nonce == m.doc.Nonce && nonce != "" @@ -940,7 +960,11 @@ StatusInfo: info, StatusData: data, } - if err := doc.validateSet(); err != nil { + // If a machine is not yet provisioned, we allow its status + // to be set back to pending (when a retry is to occur). + _, err := m.InstanceId() + allowPending := IsNotProvisionedError(err) + if err := doc.validateSet(allowPending); err != nil { return err } ops := []txn.Op{{ diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/machine_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/machine_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/machine_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/machine_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -4,10 +4,10 @@ package state_test import ( - "labix.org/v2/mgo/bson" "sort" jc "github.com/juju/testing/checkers" + "labix.org/v2/mgo/bson" gc "launchpad.net/gocheck" "launchpad.net/juju-core/constraints" @@ -223,6 +223,10 @@ c.Assert(err, jc.Satisfies, errors.IsNotFoundError) _, err = s.machine.Containers() c.Assert(err, jc.Satisfies, errors.IsNotFoundError) + include, exclude, err := s.machine.Networks() + c.Assert(err, gc.IsNil) + c.Assert(include, gc.HasLen, 0) + c.Assert(exclude, gc.HasLen, 0) err = s.machine.Remove() c.Assert(err, gc.IsNil) } @@ -348,6 +352,38 @@ c.Assert(alive, gc.Equals, false) } +func (s *MachineSuite) TestMachineNetworks(c *gc.C) { + // s.machine is created without networks, so check + // they're empty when we read them. + include, exclude, err := s.machine.Networks() + c.Assert(err, gc.IsNil) + c.Assert(include, gc.HasLen, 0) + c.Assert(exclude, gc.HasLen, 0) + + // Now create a machine with networks and read them back. + machine, err := s.State.AddOneMachine(state.MachineTemplate{ + Series: "quantal", + Jobs: []state.MachineJob{state.JobHostUnits}, + IncludeNetworks: []string{"net1", "mynet"}, + ExcludeNetworks: []string{"private-net", "logging"}, + }) + c.Assert(err, gc.IsNil) + include, exclude, err = machine.Networks() + c.Assert(err, gc.IsNil) + c.Assert(include, jc.DeepEquals, []string{"net1", "mynet"}) + c.Assert(exclude, jc.DeepEquals, []string{"private-net", "logging"}) + + // Finally, networks should be removed with the machine. + err = machine.EnsureDead() + c.Assert(err, gc.IsNil) + err = machine.Remove() + c.Assert(err, gc.IsNil) + include, exclude, err = machine.Networks() + c.Assert(err, gc.IsNil) + c.Assert(include, gc.HasLen, 0) + c.Assert(exclude, gc.HasLen, 0) +} + func (s *MachineSuite) TestMachineInstanceId(c *gc.C) { machine, err := s.State.AddMachine("quantal", state.JobHostUnits) c.Assert(err, gc.IsNil) @@ -1016,8 +1052,6 @@ func (s *MachineSuite) TestGetSetStatusWhileAlive(c *gc.C) { err := s.machine.SetStatus(params.StatusError, "", nil) c.Assert(err, gc.ErrorMatches, `cannot set status "error" without info`) - err = s.machine.SetStatus(params.StatusPending, "", nil) - c.Assert(err, gc.ErrorMatches, `cannot set status "pending"`) err = s.machine.SetStatus(params.StatusDown, "", nil) c.Assert(err, gc.ErrorMatches, `cannot set status "down"`) err = s.machine.SetStatus(params.Status("vliegkat"), "orville", nil) @@ -1050,6 +1084,16 @@ }) } +func (s *MachineSuite) TestSetStatusPending(c *gc.C) { + err := s.machine.SetStatus(params.StatusPending, "", nil) + c.Assert(err, gc.IsNil) + // Cannot set status to pending once a machine is provisioned. + err = s.machine.SetProvisioned("umbrella/0", "fake_nonce", nil) + c.Assert(err, gc.IsNil) + err = s.machine.SetStatus(params.StatusPending, "", nil) + c.Assert(err, gc.ErrorMatches, `cannot set status "pending"`) +} + func (s *MachineSuite) TestGetSetStatusWhileNotAlive(c *gc.C) { // When Dying set/get should work. err := s.machine.Destroy() diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/megawatcher.go juju-core-1.17.7/src/launchpad.net/juju-core/state/megawatcher.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/megawatcher.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/megawatcher.go 2014-03-27 15:48:42.000000000 +0000 @@ -28,8 +28,15 @@ func (m *backingMachine) updated(st *State, store *multiwatcher.Store, id interface{}) error { info := ¶ms.MachineInfo{ - Id: m.Id, + Id: m.Id, + Life: params.Life(m.Life.String()), + Series: m.Series, + Jobs: paramsJobsFromJobs(m.Jobs), + Addresses: addressesToInstanceAddresses(m.Addresses), + SupportedContainers: m.SupportedContainers, + SupportedContainersKnown: m.SupportedContainersKnown, } + oldInfo := store.Get(info.EntityId()) if oldInfo == nil { // We're adding the entry for the first time, @@ -41,17 +48,21 @@ info.Status = sdoc.Status info.StatusInfo = sdoc.StatusInfo } else { - // The entry already exists, so preserve the current status and instance id. + // The entry already exists, so preserve the current status and + // instance data. oldInfo := oldInfo.(*params.MachineInfo) info.Status = oldInfo.Status info.StatusInfo = oldInfo.StatusInfo info.InstanceId = oldInfo.InstanceId + info.HardwareCharacteristics = oldInfo.HardwareCharacteristics } - // If the machine is been provisioned, fetch the instance id if required. + // If the machine is been provisioned, fetch the instance id as required, + // and set instance id and hardware characteristics. if m.Nonce != "" && info.InstanceId == "" { instanceData, err := getInstanceData(st, m.Id) if err == nil { info.InstanceId = string(instanceData.InstanceId) + info.HardwareCharacteristics = hardwareCharacteristics(instanceData) } else if !errors.IsNotFoundError(err) { return err } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/megawatcher_internal_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/megawatcher_internal_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/megawatcher_internal_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/megawatcher_internal_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -76,10 +76,20 @@ c.Assert(m.Tag(), gc.Equals, "machine-0") err = m.SetProvisioned(instance.Id("i-"+m.Tag()), "fake_nonce", nil) c.Assert(err, gc.IsNil) + hc, err := m.HardwareCharacteristics() + c.Assert(err, gc.IsNil) + addresses := instance.NewAddresses([]string{"example.com"}) + err = m.SetAddresses(addresses) + c.Assert(err, gc.IsNil) add(¶ms.MachineInfo{ - Id: "0", - InstanceId: "i-machine-0", - Status: params.StatusPending, + Id: "0", + InstanceId: "i-machine-0", + Status: params.StatusPending, + Life: params.Alive, + Series: "quantal", + Jobs: []params.MachineJob{JobManageEnviron.ToParams()}, + Addresses: m.Addresses(), + HardwareCharacteristics: hc, }) wordpress := AddTestingService(c, s.State, "wordpress", AddTestingCharm(c, s.State, "wordpress")) @@ -95,7 +105,7 @@ Exposed: true, CharmURL: serviceCharmURL(wordpress).String(), OwnerTag: "user-admin", - Life: params.Life(Alive.String()), + Life: params.Alive, MinUnits: 3, Constraints: constraints.MustParse("mem=100M"), Config: charm.Settings{"blog-title": "boring"}, @@ -113,7 +123,7 @@ Name: "logging", CharmURL: serviceCharmURL(logging).String(), OwnerTag: "user-admin", - Life: params.Life(Alive.String()), + Life: params.Alive, Config: charm.Settings{}, }) @@ -158,11 +168,18 @@ c.Assert(err, gc.IsNil) err = m.SetStatus(params.StatusError, m.Tag(), nil) c.Assert(err, gc.IsNil) + hc, err := m.HardwareCharacteristics() + c.Assert(err, gc.IsNil) add(¶ms.MachineInfo{ - Id: fmt.Sprint(i + 1), - InstanceId: "i-" + m.Tag(), - Status: params.StatusError, - StatusInfo: m.Tag(), + Id: fmt.Sprint(i + 1), + InstanceId: "i-" + m.Tag(), + Status: params.StatusError, + StatusInfo: m.Tag(), + Life: params.Alive, + Series: "quantal", + Jobs: []params.MachineJob{JobHostUnits.ToParams()}, + Addresses: []instance.Address{}, + HardwareCharacteristics: hc, }) err = wu.AssignToMachine(m) c.Assert(err, gc.IsNil) @@ -275,6 +292,10 @@ Id: "0", Status: params.StatusError, StatusInfo: "failure", + Life: params.Alive, + Series: "quantal", + Jobs: []params.MachineJob{JobHostUnits.ToParams()}, + Addresses: []instance.Address{}, }, }, }, @@ -289,10 +310,12 @@ }, }, setUp: func(c *gc.C, st *State) { - m, err := st.AddMachine("quantal", JobManageEnviron) + m, err := st.AddMachine("trusty", JobManageEnviron) c.Assert(err, gc.IsNil) err = m.SetProvisioned("i-0", "bootstrap_nonce", nil) c.Assert(err, gc.IsNil) + err = m.SetSupportedContainers([]instance.ContainerType{instance.LXC}) + c.Assert(err, gc.IsNil) }, change: watcher.Change{ C: "machines", @@ -300,10 +323,17 @@ }, expectContents: []params.EntityInfo{ ¶ms.MachineInfo{ - Id: "0", - InstanceId: "i-0", - Status: params.StatusError, - StatusInfo: "another failure", + Id: "0", + InstanceId: "i-0", + Status: params.StatusError, + StatusInfo: "another failure", + Life: params.Alive, + Series: "trusty", + Jobs: []params.MachineJob{JobManageEnviron.ToParams()}, + Addresses: []instance.Address{}, + HardwareCharacteristics: &instance.HardwareCharacteristics{}, + SupportedContainers: []instance.ContainerType{instance.LXC}, + SupportedContainersKnown: true, }, }, }, @@ -426,7 +456,7 @@ Exposed: true, CharmURL: "local:quantal/quantal-wordpress-3", OwnerTag: "user-admin", - Life: params.Life(Alive.String()), + Life: params.Alive, MinUnits: 42, Config: charm.Settings{}, }, @@ -454,7 +484,7 @@ Name: "wordpress", CharmURL: "local:quantal/quantal-wordpress-3", OwnerTag: "user-admin", - Life: params.Life(Alive.String()), + Life: params.Alive, Constraints: constraints.MustParse("mem=99M"), Config: charm.Settings{"blog-title": "boring"}, }, @@ -481,7 +511,7 @@ Name: "wordpress", CharmURL: "local:quantal/quantal-wordpress-3", OwnerTag: "user-admin", - Life: params.Life(Alive.String()), + Life: params.Alive, Config: charm.Settings{"blog-title": "boring"}, }, }, @@ -927,7 +957,7 @@ c.Assert(err, gc.IsNil) c.Assert(m0.Id(), gc.Equals, "0") - m1, err := s.State.AddMachine("quantal", JobHostUnits) + m1, err := s.State.AddMachine("saucy", JobHostUnits) c.Assert(err, gc.IsNil) c.Assert(m1.Id(), gc.Equals, "1") @@ -938,18 +968,32 @@ s.State.StartSync() checkNext(c, w, b, []params.Delta{{ Entity: ¶ms.MachineInfo{ - Id: "0", - Status: params.StatusPending, + Id: "0", + Status: params.StatusPending, + Life: params.Alive, + Series: "quantal", + Jobs: []params.MachineJob{JobManageEnviron.ToParams()}, + Addresses: []instance.Address{}, }, }, { Entity: ¶ms.MachineInfo{ - Id: "1", - Status: params.StatusPending, + Id: "1", + Status: params.StatusPending, + Life: params.Alive, + Series: "saucy", + Jobs: []params.MachineJob{JobHostUnits.ToParams()}, + Addresses: []instance.Address{}, }, }}, "") // Make some changes to the state. - err = m0.SetProvisioned("i-0", "bootstrap_nonce", nil) + arch := "amd64" + mem := uint64(4096) + hc := &instance.HardwareCharacteristics{ + Arch: &arch, + Mem: &mem, + } + err = m0.SetProvisioned("i-0", "bootstrap_nonce", hc) c.Assert(err, gc.IsNil) err = m1.Destroy() c.Assert(err, gc.IsNil) @@ -957,7 +1001,7 @@ c.Assert(err, gc.IsNil) err = m1.Remove() c.Assert(err, gc.IsNil) - m2, err := s.State.AddMachine("quantal", JobHostUnits) + m2, err := s.State.AddMachine("trusty", JobHostUnits) c.Assert(err, gc.IsNil) c.Assert(m2.Id(), gc.Equals, "2") s.State.StartSync() @@ -976,19 +1020,32 @@ checkDeltasEqual(c, b, deltas, []params.Delta{{ Removed: true, Entity: ¶ms.MachineInfo{ - Id: "1", - Status: params.StatusPending, + Id: "1", + Status: params.StatusPending, + Life: params.Alive, + Series: "saucy", + Jobs: []params.MachineJob{JobHostUnits.ToParams()}, + Addresses: []instance.Address{}, }, }, { Entity: ¶ms.MachineInfo{ - Id: "2", - Status: params.StatusPending, + Id: "2", + Status: params.StatusPending, + Life: params.Alive, + Series: "trusty", + Jobs: []params.MachineJob{JobHostUnits.ToParams()}, + Addresses: []instance.Address{}, }, }, { Entity: ¶ms.MachineInfo{ - Id: "0", - InstanceId: "i-0", - Status: params.StatusPending, + Id: "0", + InstanceId: "i-0", + Status: params.StatusPending, + Life: params.Alive, + Series: "quantal", + Jobs: []params.MachineJob{JobManageEnviron.ToParams()}, + Addresses: []instance.Address{}, + HardwareCharacteristics: hc, }, }}) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/networks.go juju-core-1.17.7/src/launchpad.net/juju-core/state/networks.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/networks.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/networks.go 2014-03-27 15:48:42.000000000 +0000 @@ -0,0 +1,56 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package state + +import ( + "labix.org/v2/mgo" + "labix.org/v2/mgo/txn" +) + +// networksDoc represents the network restrictions for a service or machine. +// The document ID field is the globalKey of a service or a machine. +type networksDoc struct { + IncludeNetworks []string `bson:"include"` + ExcludeNetworks []string `bson:"exclude"` +} + +func newNetworksDoc(includeNetworks, excludeNetworks []string) *networksDoc { + return &networksDoc{ + IncludeNetworks: includeNetworks, + ExcludeNetworks: excludeNetworks, + } +} + +func createNetworksOp(st *State, id string, includeNetworks, excludeNetworks []string) txn.Op { + return txn.Op{ + C: st.networks.Name, + Id: id, + Assert: txn.DocMissing, + Insert: newNetworksDoc(includeNetworks, excludeNetworks), + } +} + +// While networks are immutable, there is no setNetworksOp function. + +func removeNetworksOp(st *State, id string) txn.Op { + return txn.Op{ + C: st.networks.Name, + Id: id, + Remove: true, + } +} + +func readNetworks(st *State, id string) (includeNetworks, excludeNetworks []string, err error) { + doc := networksDoc{} + if err = st.networks.FindId(id).One(&doc); err == mgo.ErrNotFound { + // In 1.17.7+ we always create a networksDoc for each service or + // machine we create, but in legacy databases this is not the + // case. We ignore the error here for backwards-compatibility. + err = nil + } else if err == nil { + includeNetworks = doc.IncludeNetworks + excludeNetworks = doc.ExcludeNetworks + } + return includeNetworks, excludeNetworks, err +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/open.go juju-core-1.17.7/src/launchpad.net/juju-core/state/open.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/open.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/open.go 2014-03-27 15:48:42.000000000 +0000 @@ -19,6 +19,7 @@ "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/errors" + "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/presence" "launchpad.net/juju-core/state/watcher" "launchpad.net/juju-core/utils" @@ -255,6 +256,7 @@ relations: db.C("relations"), relationScopes: db.C("relationscopes"), services: db.C("services"), + networks: db.C("linkednetworks"), minUnits: db.C("minunits"), settings: db.C("settings"), settingsrefs: db.C("settingsrefs"), @@ -296,6 +298,9 @@ if err := st.createAPIAddressesDoc(); err != nil { return nil, fmt.Errorf("cannot create API addresses document: %v", err) } + if err := st.createStateServingInfoDoc(); err != nil { + return nil, fmt.Errorf("cannot create state serving info document: %v", err) + } return st, nil } @@ -365,6 +370,19 @@ }} return onAbort(st.runTransaction(ops), nil) } + +// createStateServingInfoDoc creates the state serving info document +// if it does not already exist +func (st *State) createStateServingInfoDoc() error { + var info params.StateServingInfo + ops := []txn.Op{{ + C: st.stateServers.Name, + Id: stateServingInfoKey, + Assert: txn.DocMissing, + Insert: &info, + }} + return onAbort(st.runTransaction(ops), nil) +} // CACert returns the certificate used to validate the state connection. func (st *State) CACert() (cert []byte) { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/service.go juju-core-1.17.7/src/launchpad.net/juju-core/state/service.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/service.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/service.go 2014-03-27 15:48:42.000000000 +0000 @@ -224,6 +224,7 @@ Id: s.settingsKey(), Remove: true, }} + ops = append(ops, removeNetworksOp(s.st, s.globalKey())) ops = append(ops, removeConstraintsOp(s.st, s.globalKey())) return append(ops, annotationRemoveOp(s.st, s.globalKey())) } @@ -821,6 +822,11 @@ return onAbort(s.st.runTransaction(ops), errNotAlive) } +// Networks returns the networks a service is associated with. +func (s *Service) Networks() (includeNetworks, excludeNetworks []string, err error) { + return readNetworks(s.st, s.globalKey()) +} + // settingsIncRefOp returns an operation that increments the ref count // of the service settings identified by serviceName and curl. If // canCreate is false, a missing document will be treated as an error; diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/service_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/service_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/service_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/service_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -1429,3 +1429,22 @@ c.Assert(state.GetServiceOwnerTag(service), gc.Equals, "") c.Assert(service.GetOwnerTag(), gc.Equals, "user-admin") } + +func (s *ServiceSuite) TestNetworks(c *gc.C) { + service, err := s.State.Service(s.mysql.Name()) + c.Assert(err, gc.IsNil) + include, exclude, err := service.Networks() + c.Assert(err, gc.IsNil) + c.Check(include, gc.HasLen, 0) + c.Check(exclude, gc.HasLen, 0) +} + +func (s *ServiceSuite) TestNetworksOnService(c *gc.C) { + includeNetworks := []string{"yes", "on"} + excludeNetworks := []string{"no", "off"} + service := s.AddTestingServiceWithNetworks(c, "withnets", s.charm, includeNetworks, excludeNetworks) + haveIncludeNetworks, haveExcludeNetworks, err := service.Networks() + c.Assert(err, gc.IsNil) + c.Check(haveIncludeNetworks, gc.DeepEquals, includeNetworks) + c.Check(haveExcludeNetworks, gc.DeepEquals, excludeNetworks) +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/statecmd/status.go juju-core-1.17.7/src/launchpad.net/juju-core/state/statecmd/status.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/statecmd/status.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/statecmd/status.go 2014-03-27 15:48:42.000000000 +0000 @@ -346,6 +346,13 @@ status.Err = err return } + includeNetworks, excludeNetworks, err := service.Networks() + if err == nil { + status.Networks = api.NetworksSpecification{ + Enabled: includeNetworks, + Disabled: excludeNetworks, + } + } if service.IsPrincipal() { status.Units = context.processUnits(context.units[service.Name()], serviceCharmURL.String()) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/state.go juju-core-1.17.7/src/launchpad.net/juju-core/state/state.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/state.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/state.go 2014-03-27 15:48:42.000000000 +0000 @@ -25,6 +25,7 @@ "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/errors" "launchpad.net/juju-core/names" + "launchpad.net/juju-core/state/api/params" "launchpad.net/juju-core/state/multiwatcher" "launchpad.net/juju-core/state/presence" "launchpad.net/juju-core/state/watcher" @@ -54,6 +55,7 @@ relations *mgo.Collection relationScopes *mgo.Collection services *mgo.Collection + networks *mgo.Collection minUnits *mgo.Collection settings *mgo.Collection settingsrefs *mgo.Collection @@ -906,7 +908,7 @@ // AddService creates a new service, running the supplied charm, with the // supplied name (which must be unique). If the charm defines peer relations, // they will be created automatically. -func (st *State) AddService(name, ownerTag string, ch *Charm) (service *Service, err error) { +func (st *State) AddService(name, ownerTag string, ch *Charm, includeNetworks, excludeNetworks []string) (service *Service, err error) { defer utils.ErrorContextf(&err, "cannot add service %q", name) kind, ownerId, err := names.ParseTag(ownerTag, names.UserTagKind) if err != nil || kind != names.UserTagKind { @@ -950,6 +952,7 @@ ops := []txn.Op{ env.assertAliveOp(), createConstraintsOp(st, svc.globalKey(), constraints.Value{}), + createNetworksOp(st, svc.globalKey(), includeNetworks, excludeNetworks), createSettingsOp(st, svc.settingsKey(), nil), { C: st.users.Name, @@ -1381,7 +1384,7 @@ VotingMachineIds []string } -// StateServerInfo returns returns information about +// StateServerInfo returns information about // the currently configured state server machines. func (st *State) StateServerInfo() (*StateServerInfo, error) { var doc stateServersDoc @@ -1395,6 +1398,31 @@ }, nil } +const stateServingInfoKey = "stateServingInfo" + +// StateServingInfo returns information for running a state server machine +func (st *State) StateServingInfo() (params.StateServingInfo, error) { + var info params.StateServingInfo + err := st.stateServers.Find(bson.D{{"_id", stateServingInfoKey}}).One(&info) + if err != nil { + return info, err + } + return info, nil +} + +// SetStateServingInfo stores information needed for running a state server +func (st *State) SetStateServingInfo(info params.StateServingInfo) error { + ops := []txn.Op{{ + C: st.stateServers.Name, + Id: stateServingInfoKey, + Update: bson.D{{"$set", info}}, + }} + if err := st.runTransaction(ops); err != nil { + return fmt.Errorf("cannot set state serving info: %v", err) + } + return nil +} + // ResumeTransactions resumes all pending transactions. func (st *State) ResumeTransactions() error { return st.runner.ResumeAll() diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/state_test.go juju-core-1.17.7/src/launchpad.net/juju-core/state/state_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/state_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/state_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -964,19 +964,19 @@ func (s *StateSuite) TestAddService(c *gc.C) { charm := s.AddTestingCharm(c, "dummy") - _, err := s.State.AddService("haha/borken", "user-admin", charm) + _, err := s.State.AddService("haha/borken", "user-admin", charm, nil, nil) c.Assert(err, gc.ErrorMatches, `cannot add service "haha/borken": invalid name`) _, err = s.State.Service("haha/borken") c.Assert(err, gc.ErrorMatches, `"haha/borken" is not a valid service name`) // set that a nil charm is handled correctly - _, err = s.State.AddService("umadbro", "user-admin", nil) + _, err = s.State.AddService("umadbro", "user-admin", nil, nil, nil) c.Assert(err, gc.ErrorMatches, `cannot add service "umadbro": charm is nil`) - wordpress, err := s.State.AddService("wordpress", "user-admin", charm) + wordpress, err := s.State.AddService("wordpress", "user-admin", charm, nil, nil) c.Assert(err, gc.IsNil) c.Assert(wordpress.Name(), gc.Equals, "wordpress") - mysql, err := s.State.AddService("mysql", "user-admin", charm) + mysql, err := s.State.AddService("mysql", "user-admin", charm, nil, nil) c.Assert(err, gc.IsNil) c.Assert(mysql.Name(), gc.Equals, "mysql") @@ -1003,7 +1003,7 @@ c.Assert(err, gc.IsNil) err = env.Destroy() c.Assert(err, gc.IsNil) - _, err = s.State.AddService("s1", "user-admin", charm) + _, err = s.State.AddService("s1", "user-admin", charm, nil, nil) c.Assert(err, gc.ErrorMatches, `cannot add service "s1": environment is no longer alive`) } @@ -1018,7 +1018,7 @@ c.Assert(env.Life(), gc.Equals, state.Alive) c.Assert(env.Destroy(), gc.IsNil) }).Check() - _, err = s.State.AddService("s1", "user-admin", charm) + _, err = s.State.AddService("s1", "user-admin", charm, nil, nil) c.Assert(err, gc.ErrorMatches, `cannot add service "s1": environment is no longer alive`) } @@ -1030,19 +1030,19 @@ func (s *StateSuite) TestAddServiceNoTag(c *gc.C) { charm := s.AddTestingCharm(c, "dummy") - _, err := s.State.AddService("wordpress", state.AdminUser, charm) + _, err := s.State.AddService("wordpress", state.AdminUser, charm, nil, nil) c.Assert(err, gc.ErrorMatches, "cannot add service \"wordpress\": Invalid ownertag admin") } func (s *StateSuite) TestAddServiceNotUserTag(c *gc.C) { charm := s.AddTestingCharm(c, "dummy") - _, err := s.State.AddService("wordpress", "machine-3", charm) + _, err := s.State.AddService("wordpress", "machine-3", charm, nil, nil) c.Assert(err, gc.ErrorMatches, "cannot add service \"wordpress\": Invalid ownertag machine-3") } func (s *StateSuite) TestAddServiceNonExistentUser(c *gc.C) { charm := s.AddTestingCharm(c, "dummy") - _, err := s.State.AddService("wordpress", "user-notAuser", charm) + _, err := s.State.AddService("wordpress", "user-notAuser", charm, nil, nil) c.Assert(err, gc.ErrorMatches, "cannot add service \"wordpress\": user notAuser doesn't exist") } @@ -1053,13 +1053,13 @@ c.Assert(len(services), gc.Equals, 0) // Check that after adding services the result is ok. - _, err = s.State.AddService("wordpress", "user-admin", charm) + _, err = s.State.AddService("wordpress", "user-admin", charm, nil, nil) c.Assert(err, gc.IsNil) services, err = s.State.AllServices() c.Assert(err, gc.IsNil) c.Assert(len(services), gc.Equals, 1) - _, err = s.State.AddService("mysql", "user-admin", charm) + _, err = s.State.AddService("mysql", "user-admin", charm, nil, nil) c.Assert(err, gc.IsNil) services, err = s.State.AllServices() c.Assert(err, gc.IsNil) @@ -2524,7 +2524,7 @@ // Add a service and 4 units: one with a different version, one // with an empty version, one with the current version, and one // with the new version. - service, err := s.State.AddService("wordpress", "user-admin", s.AddTestingCharm(c, "wordpress")) + service, err := s.State.AddService("wordpress", "user-admin", s.AddTestingCharm(c, "wordpress"), nil, nil) c.Assert(err, gc.IsNil) unit0, err := service.AddUnit() c.Assert(err, gc.IsNil) @@ -2574,7 +2574,7 @@ // Add a machine and a unit with the current version. machine, err := s.State.AddMachine("series", state.JobHostUnits) c.Assert(err, gc.IsNil) - service, err := s.State.AddService("wordpress", "user-admin", s.AddTestingCharm(c, "wordpress")) + service, err := s.State.AddService("wordpress", "user-admin", s.AddTestingCharm(c, "wordpress"), nil, nil) c.Assert(err, gc.IsNil) unit, err := service.AddUnit() c.Assert(err, gc.IsNil) @@ -2850,6 +2850,43 @@ return &i } +func (s *StateSuite) TestStateServingInfo(c *gc.C) { + info, err := s.State.StateServingInfo() + c.Assert(info, jc.DeepEquals, params.StateServingInfo{}) + // no error because empty doc created by default + c.Assert(err, gc.IsNil) + + data := params.StateServingInfo{ + APIPort: 69, + StatePort: 80, + Cert: "Some cert", + PrivateKey: "Some key", + SharedSecret: "Some Keyfile", + } + err = s.State.SetStateServingInfo(data) + c.Assert(err, gc.IsNil) + + info, err = s.State.StateServingInfo() + c.Assert(err, gc.IsNil) + c.Assert(info, jc.DeepEquals, data) +} + +func (s *StateSuite) TestOpenCreatesStateServingInfoDoc(c *gc.C) { + // Delete the stateServers collection to pretend this + // is an older environment that had not created it + // already. + err := s.stateServers.DropCollection() + c.Assert(err, gc.IsNil) + + st, err := state.Open(state.TestingStateInfo(), state.TestingDialOpts(), state.Policy(nil)) + c.Assert(err, gc.IsNil) + defer st.Close() + + info, err := st.StateServingInfo() + c.Assert(err, gc.IsNil) + c.Assert(info, gc.DeepEquals, params.StateServingInfo{}) +} + func (s *StateSuite) TestSetAPIHostPorts(c *gc.C) { addrs, err := s.State.APIHostPorts() c.Assert(err, gc.IsNil) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/status.go juju-core-1.17.7/src/launchpad.net/juju-core/state/status.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/status.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/status.go 2014-03-27 15:48:42.000000000 +0000 @@ -26,12 +26,16 @@ // validateSet returns an error if the statusDoc does not represent a sane // SetStatus operation. -func (doc statusDoc) validateSet() error { +func (doc statusDoc) validateSet(allowPending bool) error { if !doc.Status.Valid() { return fmt.Errorf("cannot set invalid status %q", doc.Status) } switch doc.Status { - case params.StatusPending, params.StatusDown: + case params.StatusPending: + if !allowPending { + return fmt.Errorf("cannot set status %q", doc.Status) + } + case params.StatusDown: return fmt.Errorf("cannot set status %q", doc.Status) case params.StatusError: if doc.StatusInfo == "" { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/state/unit.go juju-core-1.17.7/src/launchpad.net/juju-core/state/unit.go --- juju-core-1.17.6/src/launchpad.net/juju-core/state/unit.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/state/unit.go 2014-03-27 15:48:42.000000000 +0000 @@ -562,7 +562,7 @@ StatusInfo: info, StatusData: data, } - if err := doc.validateSet(); err != nil { + if err := doc.validateSet(false); err != nil { return err } ops := []txn.Op{{ @@ -991,11 +991,12 @@ return &cons, nil } -// AssignToNewMachineOrContainer assigns the unit to a new machine, with constraints -// determined according to the service and environment constraints at the time of unit creation. -// If a container is required, a clean, empty machine instance is required on which to create -// the container. An existing clean, empty instance is first searched for, and if not found, -// a new one is created. +// AssignToNewMachineOrContainer assigns the unit to a new machine, +// with constraints determined according to the service and +// environment constraints at the time of unit creation. If a +// container is required, a clean, empty machine instance is required +// on which to create the container. An existing clean, empty instance +// is first searched for, and if not found, a new one is created. func (u *Unit) AssignToNewMachineOrContainer() (err error) { defer assignContextf(&err, u, "new machine or container") if u.doc.Principal != "" { @@ -1026,10 +1027,20 @@ } else if err != nil { return err } + svc, err := u.Service() + if err != nil { + return err + } + includeNetworks, excludeNetworks, err := svc.Networks() + if err != nil { + return err + } template := MachineTemplate{ - Series: u.doc.Series, - Constraints: *cons, - Jobs: []MachineJob{JobHostUnits}, + Series: u.doc.Series, + Constraints: *cons, + Jobs: []MachineJob{JobHostUnits}, + IncludeNetworks: includeNetworks, + ExcludeNetworks: excludeNetworks, } err = u.assignToNewMachine(template, host.Id, *cons.Container) if err == machineNotCleanErr { @@ -1059,10 +1070,20 @@ if cons.HasContainer() { containerType = *cons.Container } + svc, err := u.Service() + if err != nil { + return err + } + includeNetworks, excludeNetworks, err := svc.Networks() + if err != nil { + return err + } template := MachineTemplate{ - Series: u.doc.Series, - Constraints: *cons, - Jobs: []MachineJob{JobHostUnits}, + Series: u.doc.Series, + Constraints: *cons, + Jobs: []MachineJob{JobHostUnits}, + IncludeNetworks: includeNetworks, + ExcludeNetworks: excludeNetworks, } return u.assignToNewMachine(template, "", containerType) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/store/lpad.go juju-core-1.17.7/src/launchpad.net/juju-core/store/lpad.go --- juju-core-1.17.6/src/launchpad.net/juju-core/store/lpad.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/store/lpad.go 2014-03-27 15:48:42.000000000 +0000 @@ -69,7 +69,10 @@ urls := []*charm.URL{curl} schema, name := curl.Schema, curl.Name for _, series := range tip.OfficialSeries { - curl = &charm.URL{Schema: schema, Name: name, Series: series, Revision: -1} + curl = &charm.URL{ + Reference: charm.Reference{Schema: schema, Name: name, Revision: -1}, + Series: series, + } curl.Series = series curl.User = "" urls = append(urls, curl) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/store/server.go juju-core-1.17.7/src/launchpad.net/juju-core/store/server.go --- juju-core-1.17.6/src/launchpad.net/juju-core/store/server.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/store/server.go 2014-03-27 15:48:42.000000000 +0000 @@ -16,6 +16,8 @@ "launchpad.net/juju-core/log" ) +const DefaultSeries = "precise" + // Server is an http.Handler that serves the HTTP API of juju // so that juju clients can retrieve published charms. type Server struct { @@ -74,6 +76,24 @@ return []string{kind, curl.Series, curl.Name, curl.User} } +func (s *Server) resolveURL(url string) (*charm.URL, error) { + ref, series, err := charm.ParseReference(url) + if err != nil { + return nil, err + } + if series == "" { + prefSeries, err := s.store.Series(ref) + if err != nil { + return nil, err + } + if len(prefSeries) == 0 { + return nil, ErrNotFound + } + return &charm.URL{Reference: ref, Series: prefSeries[0]}, nil + } + return &charm.URL{Reference: ref, Series: series}, nil +} + func (s *Server) serveInfo(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/charm-info" { w.WriteHeader(http.StatusNotFound) @@ -84,7 +104,7 @@ for _, url := range r.Form["charms"] { c := &charm.InfoResponse{} response[url] = c - curl, err := charm.ParseURL(url) + curl, err := s.resolveURL(url) var info *CharmInfo if err == nil { info, err = s.store.CharmInfo(curl) @@ -92,11 +112,12 @@ var skey []string if err == nil { skey = charmStatsKey(curl, "charm-info") + c.CanonicalURL = curl.String() c.Sha256 = info.BundleSha256() c.Revision = info.Revision() c.Digest = info.Digest() } else { - if err == ErrNotFound { + if err == ErrNotFound && curl != nil { skey = charmStatsKey(curl, "charm-missing") } c.Errors = append(c.Errors, err.Error()) @@ -132,7 +153,7 @@ } c := &charm.EventResponse{} response[url] = c - curl, err := charm.ParseURL(url) + curl, err := s.resolveURL(url) var event *CharmEvent if err == nil { event, err = s.store.CharmEvent(curl, digest) @@ -169,7 +190,7 @@ if !strings.HasPrefix(r.URL.Path, "/charm/") { panic("serveCharm: bad url") } - curl, err := charm.ParseURL("cs:" + r.URL.Path[len("/charm/"):]) + curl, err := s.resolveURL("cs:" + r.URL.Path[len("/charm/"):]) if err != nil { w.WriteHeader(http.StatusNotFound) return diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/store/server_test.go juju-core-1.17.7/src/launchpad.net/juju-core/store/server_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/store/server_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/store/server_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -21,7 +21,7 @@ ) func (s *StoreSuite) prepareServer(c *gc.C) (*store.Server, *charm.URL) { - curl := charm.MustParseURL("cs:oneiric/wordpress") + curl := charm.MustParseURL("cs:precise/wordpress") pub, err := s.store.CharmPublisher([]*charm.URL{curl}, "some-digest") c.Assert(err, gc.IsNil) err = pub.Publish(&FakeCharmDir{}) @@ -37,10 +37,12 @@ req, err := http.NewRequest("GET", "/charm-info", nil) c.Assert(err, gc.IsNil) - var tests = []struct{ url, sha, digest, err string }{ - {curl.String(), fakeRevZeroSha, "some-digest", ""}, - {"cs:oneiric/non-existent", "", "", "entry not found"}, - {"cs:bad", "", "", `charm URL without series: "cs:bad"`}, + var tests = []struct{ url, canonical, sha, digest, err string }{ + {curl.String(), curl.String(), fakeRevZeroSha, "some-digest", ""}, + {"cs:oneiric/non-existent", "", "", "", "entry not found"}, + {"cs:wordpress", curl.String(), fakeRevZeroSha, "some-digest", ""}, + {"cs:/bad", "", "", "", `charm URL has invalid series: "cs:/bad"`}, + {"gopher:archie-server", "", "", "", `charm URL has invalid schema: "gopher:archie-server"`}, } for _, t := range tests { @@ -51,9 +53,10 @@ expected := make(map[string]interface{}) if t.sha != "" { expected[t.url] = map[string]interface{}{ - "revision": float64(0), - "sha256": t.sha, - "digest": t.digest, + "canonical-url": t.canonical, + "revision": float64(0), + "sha256": t.sha, + "digest": t.digest, } } else { expected[t.url] = map[string]interface{}{ @@ -64,11 +67,12 @@ obtained := map[string]interface{}{} err = json.NewDecoder(rec.Body).Decode(&obtained) c.Assert(err, gc.IsNil) - c.Assert(obtained, gc.DeepEquals, expected) + c.Assert(obtained, gc.DeepEquals, expected, gc.Commentf("URL: %s", t.url)) c.Assert(rec.Header().Get("Content-Type"), gc.Equals, "application/json") } - s.checkCounterSum(c, []string{"charm-info", curl.Series, curl.Name}, false, 1) + // 2 charm-info events, one for resolved URL, one for the reference. + s.checkCounterSum(c, []string{"charm-info", curl.Series, curl.Name}, false, 2) s.checkCounterSum(c, []string{"charm-missing", "oneiric", "non-existent"}, false, 1) } @@ -179,6 +183,23 @@ s.checkCounterSum(c, []string{"charm-event", "oneiric", "mysql"}, false, 1) } +func (s *StoreSuite) TestSeriesNotFound(c *gc.C) { + server, err := store.NewServer(s.store) + req, err := http.NewRequest("GET", "/charm-info?charms=cs:not-found", nil) + c.Assert(err, gc.IsNil) + rec := httptest.NewRecorder() + server.ServeHTTP(rec, req) + c.Assert(rec.Code, gc.Equals, http.StatusOK) + + expected := map[string]interface{}{"cs:not-found": map[string]interface{}{ + "revision": float64(0), + "errors": []interface{}{"entry not found"}}} + obtained := map[string]interface{}{} + err = json.NewDecoder(rec.Body).Decode(&obtained) + c.Assert(err, gc.IsNil) + c.Assert(obtained, gc.DeepEquals, expected) +} + // checkCounterSum checks that statistics are properly collected. // It retries a few times as they are generally collected in background. func (s *StoreSuite) checkCounterSum(c *gc.C, key []string, prefix bool, expected int64) { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/store/store.go juju-core-1.17.7/src/launchpad.net/juju-core/store/store.go --- juju-core-1.17.6/src/launchpad.net/juju-core/store/store.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/store/store.go 2014-03-27 15:48:42.000000000 +0000 @@ -713,6 +713,63 @@ return ci.config } +var ltsReleases = map[string]bool{ + "lucid": true, + "precise": true, + "trusty": true, +} + +type byPreferredSeries []string + +func (s byPreferredSeries) Len() int { return len(s) } +func (s byPreferredSeries) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s byPreferredSeries) Less(i, j int) bool { + _, iLts := ltsReleases[s[i]] + _, jLts := ltsReleases[s[j]] + if iLts == jLts { + return sort.StringSlice(s).Less(j, i) + } + return iLts +} + +// Series returns all the series available for a charm reference, in descending +// order of preference. LTS releases preferred over non-LTS +func (s *Store) Series(ref charm.Reference) ([]string, error) { + session := s.session.Copy() + defer session.Close() + + patternURL := &charm.URL{Reference: ref, Series: "[a-z][^/]+"} + patternURL = patternURL.WithRevision(-1) + + charms := session.Charms() + q := charms.Find(bson.M{ + "urls": bson.RegEx{Pattern: fmt.Sprintf("^%s$", patternURL.String())}, + }) + var cdocs []charmDoc + err := q.All(&cdocs) + if err != nil { + return nil, err + } + + // Unique set of series + seriesSet := make(map[string]bool) + for _, cdoc := range cdocs { + for _, url := range cdoc.URLs { + if ref == url.Reference { + seriesSet[url.Series] = true + } + } + } + + // Collect into a slice + var result []string + for series := range seriesSet { + result = append(result, series) + } + sort.Sort(byPreferredSeries(result)) + return result, nil +} + // getRevisions returns at most the last n revisions for charm at url, // in descending revision order. For limit n=0, all revisions are returned. func (s *Store) getRevisions(url *charm.URL, n int) ([]*CharmInfo, error) { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/store/store_test.go juju-core-1.17.7/src/launchpad.net/juju-core/store/store_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/store/store_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/store/store_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -335,6 +335,109 @@ c.Check(lock3, gc.IsNil) } +var seriesSolverCharms = []struct { + series, name string +}{ + {"oneiric", "wordpress"}, + {"precise", "wordpress"}, + {"quantal", "wordpress"}, + {"trusty", "wordpress"}, + {"volumetric", "wordpress"}, + + {"precise", "mysql"}, + {"trusty", "mysqladmin"}, + + {"def", "zebra"}, + {"zef", "zebra"}, +} + +func (s *StoreSuite) TestSeriesSolver(c *gc.C) { + for _, t := range seriesSolverCharms { + url := charm.MustParseURL(fmt.Sprintf("cs:%s/%s", t.series, t.name)) + urls := []*charm.URL{url} + + pub, err := s.store.CharmPublisher(urls, fmt.Sprintf("some-%s-%s-digest", t.series, t.name)) + c.Assert(err, gc.IsNil) + c.Assert(pub.Revision(), gc.Equals, 0) + + err = pub.Publish(&FakeCharmDir{}) + c.Assert(err, gc.IsNil) + } + + // LTS, then non-LTS, reverse alphabetical order + ref, _, err := charm.ParseReference("cs:wordpress") + c.Assert(err, gc.IsNil) + series, err := s.store.Series(ref) + c.Assert(err, gc.IsNil) + c.Assert(series, gc.HasLen, 5) + c.Check(series[0], gc.Equals, "trusty") + c.Check(series[1], gc.Equals, "precise") + c.Check(series[2], gc.Equals, "volumetric") + c.Check(series[3], gc.Equals, "quantal") + c.Check(series[4], gc.Equals, "oneiric") + + // Ensure that the full charm name matches, not just prefix + ref, _, err = charm.ParseReference("cs:mysql") + c.Assert(err, gc.IsNil) + series, err = s.store.Series(ref) + c.Assert(err, gc.IsNil) + c.Assert(series, gc.HasLen, 1) + c.Check(series[0], gc.Equals, "precise") + + // No LTS, reverse alphabetical order + ref, _, err = charm.ParseReference("cs:zebra") + c.Assert(err, gc.IsNil) + series, err = s.store.Series(ref) + c.Assert(err, gc.IsNil) + c.Assert(series, gc.HasLen, 2) + c.Check(series[0], gc.Equals, "zef") + c.Check(series[1], gc.Equals, "def") +} + +var mysqlSeriesCharms = []struct { + fakeDigest string + urls []string +}{ + {"533224069221503992aaa726", []string{"cs:~charmers/oneiric/mysql", "cs:oneiric/mysql"}}, + {"533224c79221503992aaa7ea", []string{"cs:~charmers/precise/mysql", "cs:precise/mysql"}}, + {"533223a69221503992aaa6be", []string{"cs:~bjornt/trusty/mysql"}}, + {"533225b49221503992aaa8e5", []string{"cs:~clint-fewbar/precise/mysql"}}, + {"5332261b9221503992aaa96b", []string{"cs:~gandelman-a/precise/mysql"}}, + {"533226289221503992aaa97d", []string{"cs:~gandelman-a/quantal/mysql"}}, + {"5332264d9221503992aaa9b0", []string{"cs:~hazmat/precise/mysql"}}, + {"5332272d9221503992aaaa4d", []string{"cs:~jmit/oneiric/mysql"}}, + {"53328a439221503992aaad28", []string{"cs:~landscape/trusty/mysql"}}, + {"533228ae9221503992aaab96", []string{"cs:~negronjl/precise/mysql-file-permissions"}}, + {"533228f39221503992aaabde", []string{"cs:~openstack-ubuntu-testing/oneiric/mysql"}}, + {"533229029221503992aaabed", []string{"cs:~openstack-ubuntu-testing/precise/mysql"}}, + {"5332291e9221503992aaac09", []string{"cs:~openstack-ubuntu-testing/quantal/mysql"}}, + {"53327f4f9221503992aaad1e", []string{"cs:~tribaal/trusty/mysql"}}, +} + +func (s *StoreSuite) TestMysqlSeriesSolver(c *gc.C) { + for _, t := range mysqlSeriesCharms { + var urls []*charm.URL + for _, url := range t.urls { + urls = append(urls, charm.MustParseURL(url)) + } + + pub, err := s.store.CharmPublisher(urls, t.fakeDigest) + c.Assert(err, gc.IsNil) + c.Assert(pub.Revision(), gc.Equals, 0) + + err = pub.Publish(&FakeCharmDir{}) + c.Assert(err, gc.IsNil) + } + + ref, _, err := charm.ParseReference("cs:mysql") + c.Assert(err, gc.IsNil) + series, err := s.store.Series(ref) + c.Assert(err, gc.IsNil) + c.Assert(series, gc.HasLen, 2) + c.Check(series[0], gc.Equals, "precise") + c.Check(series[1], gc.Equals, "oneiric") +} + func (s *StoreSuite) TestConflictingUpdate(c *gc.C) { // This test checks that if for whatever reason the locking // safety-net fails, adding two charms in parallel still diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/testing/charm.go juju-core-1.17.7/src/launchpad.net/juju-core/testing/charm.go --- juju-core-1.17.6/src/launchpad.net/juju-core/testing/charm.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/testing/charm.go 2014-03-27 15:48:42.000000000 +0000 @@ -96,10 +96,12 @@ } clone(dst, r.DirPath(name)) return &charm.URL{ - Schema: "local", - Series: series, - Name: name, - Revision: -1, + Reference: charm.Reference{ + Schema: "local", + Name: name, + Revision: -1, + }, + Series: series, } } @@ -126,9 +128,10 @@ // MockCharmStore implements charm.Repository and is used to isolate tests // that would otherwise need to hit the real charm store. type MockCharmStore struct { - charms map[string]map[int]*charm.Bundle - AuthAttrs string - TestMode bool + charms map[string]map[int]*charm.Bundle + AuthAttrs string + TestMode bool + DefaultSeries string } func NewMockCharmStore() *MockCharmStore { @@ -145,6 +148,18 @@ return s } +func (s *MockCharmStore) WithDefaultSeries(series string) charm.Repository { + s.DefaultSeries = series + return s +} + +func (s *MockCharmStore) Resolve(ref charm.Reference) (*charm.URL, error) { + if s.DefaultSeries == "" { + return nil, fmt.Errorf("missing default series, cannot resolve charm url: %q", ref) + } + return &charm.URL{Reference: ref, Series: s.DefaultSeries}, nil +} + // SetCharm adds and removes charms in s. The affected charm is identified by // charmURL, which must be revisioned. If bundle is nil, the charm will be // removed; otherwise, it will be stored. It is an error to store a bundle diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/dotprofile.go juju-core-1.17.7/src/launchpad.net/juju-core/upgrades/dotprofile.go --- juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/dotprofile.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/upgrades/dotprofile.go 2014-03-27 15:48:42.000000000 +0000 @@ -0,0 +1,37 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package upgrades + +import ( + "fmt" + + "launchpad.net/juju-core/utils/exec" +) + +// As of the middle of the 1.17 cycle, the proxy settings are written out to +// /home/ubuntu/.juju-proxy both by cloud-init and the machine environ worker. +// An older version of juju that has been upgraded will get the proxy settings +// written out to the .juju-proxy file, but the .profile for the ubuntu user +// wouldn't have been updated to source this file. +// +// This upgrade step is to add the line to source the file if it is missing +// from the file. +func ensureUbuntuDotProfileSourcesProxyFile(context Context) error { + // We look to see if the proxy line is there already as the manual + // provider may have had it aleady. The ubuntu user may not exist + // (local provider only). + command := fmt.Sprintf(""+ + `([ ! -e %s/.profile ] || grep -q '.juju-proxy' %s/.profile) || `+ + `printf '\n# Added by juju\n[ -f "$HOME/.juju-proxy" ] && . "$HOME/.juju-proxy"\n' >> %s/.profile`, + ubuntuHome, ubuntuHome, ubuntuHome) + logger.Tracef("command: %s", command) + result, err := exec.RunCommands(exec.RunParams{ + Commands: command, + }) + if err != nil { + return err + } + logger.Tracef("stdout: %s", result.Stdout) + return nil +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/dotprofile_test.go juju-core-1.17.7/src/launchpad.net/juju-core/upgrades/dotprofile_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/dotprofile_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/upgrades/dotprofile_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -0,0 +1,82 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package upgrades_test + +import ( + "io/ioutil" + "path" + + "github.com/juju/loggo" + jc "github.com/juju/testing/checkers" + gc "launchpad.net/gocheck" + + "launchpad.net/juju-core/testing" + "launchpad.net/juju-core/upgrades" +) + +type ensureDotProfileSuite struct { + testing.FakeHomeSuite + home string + ctx upgrades.Context +} + +var _ = gc.Suite(&ensureDotProfileSuite{}) + +func (s *ensureDotProfileSuite) SetUpTest(c *gc.C) { + s.FakeHomeSuite.SetUpTest(c) + + loggo.GetLogger("juju.upgrade").SetLogLevel(loggo.TRACE) + + s.home = c.MkDir() + s.PatchValue(upgrades.UbuntuHome, s.home) + s.ctx = &mockContext{} +} + +const expectedLine = ` +# Added by juju +[ -f "$HOME/.juju-proxy" ] && . "$HOME/.juju-proxy" +` + +func (s *ensureDotProfileSuite) writeDotProfile(c *gc.C, content string) { + dotProfile := path.Join(s.home, ".profile") + err := ioutil.WriteFile(dotProfile, []byte(content), 0644) + c.Assert(err, gc.IsNil) +} + +func (s *ensureDotProfileSuite) assertProfile(c *gc.C, content string) { + dotProfile := path.Join(s.home, ".profile") + data, err := ioutil.ReadFile(dotProfile) + c.Assert(err, gc.IsNil) + c.Assert(string(data), gc.Equals, content) +} + +func (s *ensureDotProfileSuite) TestSourceAdded(c *gc.C) { + s.writeDotProfile(c, "") + err := upgrades.EnsureUbuntuDotProfileSourcesProxyFile(s.ctx) + c.Assert(err, gc.IsNil) + s.assertProfile(c, expectedLine) +} + +func (s *ensureDotProfileSuite) TestIdempotent(c *gc.C) { + s.writeDotProfile(c, "") + err := upgrades.EnsureUbuntuDotProfileSourcesProxyFile(s.ctx) + c.Assert(err, gc.IsNil) + err = upgrades.EnsureUbuntuDotProfileSourcesProxyFile(s.ctx) + c.Assert(err, gc.IsNil) + s.assertProfile(c, expectedLine) +} + +func (s *ensureDotProfileSuite) TestProfileUntouchedIfJujuProxyInSource(c *gc.C) { + content := "source .juju-proxy\n" + s.writeDotProfile(c, content) + err := upgrades.EnsureUbuntuDotProfileSourcesProxyFile(s.ctx) + c.Assert(err, gc.IsNil) + s.assertProfile(c, content) +} + +func (s *ensureDotProfileSuite) TestSkippedIfDotProfileDoesntExist(c *gc.C) { + err := upgrades.EnsureUbuntuDotProfileSourcesProxyFile(s.ctx) + c.Assert(err, gc.IsNil) + c.Assert(path.Join(s.home, ".profile"), jc.DoesNotExist) +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/export_test.go juju-core-1.17.7/src/launchpad.net/juju-core/upgrades/export_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/export_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/upgrades/export_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -13,10 +13,11 @@ IsLocalEnviron = &isLocalEnviron // 118 upgrade functions - StepsFor118 = stepsFor118 - EnsureLockDirExistsAndUbuntuWritable = ensureLockDirExistsAndUbuntuWritable - EnsureSystemSSHKey = ensureSystemSSHKey - UpdateRsyslogPort = updateRsyslogPort - ProcessDeprecatedEnvSettings = processDeprecatedEnvSettings - MigrateLocalProviderAgentConfig = migrateLocalProviderAgentConfig + StepsFor118 = stepsFor118 + EnsureLockDirExistsAndUbuntuWritable = ensureLockDirExistsAndUbuntuWritable + EnsureSystemSSHKey = ensureSystemSSHKey + EnsureUbuntuDotProfileSourcesProxyFile = ensureUbuntuDotProfileSourcesProxyFile + UpdateRsyslogPort = updateRsyslogPort + ProcessDeprecatedEnvSettings = processDeprecatedEnvSettings + MigrateLocalProviderAgentConfig = migrateLocalProviderAgentConfig ) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/steps118.go juju-core-1.17.7/src/launchpad.net/juju-core/upgrades/steps118.go --- juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/steps118.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/upgrades/steps118.go 2014-03-27 15:48:42.000000000 +0000 @@ -8,7 +8,7 @@ return []Step{ &upgradeStep{ description: "make $DATADIR/locks owned by ubuntu:ubuntu", - targets: []Target{HostMachine}, + targets: []Target{AllMachines}, run: ensureLockDirExistsAndUbuntuWritable, }, &upgradeStep{ @@ -36,5 +36,10 @@ targets: []Target{StateServer}, run: migrateLocalProviderAgentConfig, }, + &upgradeStep{ + description: "make /home/ubuntu/.profile source .juju-proxy file", + targets: []Target{AllMachines}, + run: ensureUbuntuDotProfileSourcesProxyFile, + }, } } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/steps118_test.go juju-core-1.17.7/src/launchpad.net/juju-core/upgrades/steps118_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/upgrades/steps118_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/upgrades/steps118_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -23,6 +23,7 @@ "install rsyslog-gnutls", "remove deprecated environment config settings", "migrate local provider agent config", + "make /home/ubuntu/.profile source .juju-proxy file", } func (s *steps118Suite) TestUpgradeOperationsContent(c *gc.C) { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/utils/apt.go juju-core-1.17.7/src/launchpad.net/juju-core/utils/apt.go --- juju-core-1.17.6/src/launchpad.net/juju-core/utils/apt.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/utils/apt.go 2014-03-27 15:48:42.000000000 +0000 @@ -81,16 +81,6 @@ } } -// AptGetCommand returns a command to execute apt-get -// with the specified arguments, and the appropriate -// environment variables and options for a non-interactive -// session. -func AptGetCommand(args ...string) []string { - cmd := append([]string{"env"}, aptGetEnvOptions...) - cmd = append(cmd, aptGetCommand...) - return append(cmd, args...) -} - // AptGetPreparePackages returns a slice of installCommands. Each item // in the slice is suitable for passing directly to AptGetInstall. // diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/utils/apt_test.go juju-core-1.17.7/src/launchpad.net/juju-core/utils/apt_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/utils/apt_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/utils/apt_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -39,22 +39,6 @@ c.Assert(packagesList[1], gc.DeepEquals, []string{"bridge-utils", "git"}) } -func (s *AptSuite) TestAptGetCommand(c *gc.C) { - s.testAptGetCommand(c) - s.testAptGetCommand(c, "install", "foo") -} - -func (s *AptSuite) testAptGetCommand(c *gc.C, args ...string) { - commonArgs := []string{ - "env", "DEBIAN_FRONTEND=noninteractive", - "apt-get", "--option=Dpkg::Options::=--force-confold", - "--option=Dpkg::options::=--force-unsafe-io", "--assume-yes", "--quiet", - } - expected := append(commonArgs, args...) - cmd := utils.AptGetCommand(args...) - c.Assert(cmd, gc.DeepEquals, expected) -} - func (s *AptSuite) TestAptGetError(c *gc.C) { const expected = `E: frobnicator failure detected` cmdError := fmt.Errorf("error") diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/utils/http.go juju-core-1.17.7/src/launchpad.net/juju-core/utils/http.go --- juju-core-1.17.6/src/launchpad.net/juju-core/utils/http.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/utils/http.go 2014-03-27 15:48:42.000000000 +0000 @@ -12,7 +12,54 @@ var insecureClient = (*http.Client)(nil) var insecureClientMutex = sync.Mutex{} +func init() { + // See https://code.google.com/p/go/issues/detail?id=4677 + // We need to force the connection to close each time so that we don't + // hit the above Go bug. + defaultTransport := http.DefaultTransport.(*http.Transport) + defaultTransport.DisableKeepAlives = true + registerFileProtocol(defaultTransport) +} + +// registerFileProtocol registers support for file:// URLs on the given transport. +func registerFileProtocol(transport *http.Transport) { + transport.RegisterProtocol("file", http.NewFileTransport(http.Dir("/"))) +} + +// SSLHostnameVerification is used as a switch for when a given provider might +// use self-signed credentials and we should not try to verify the hostname on +// the TLS/SSL certificates +type SSLHostnameVerification bool + +const ( + // VerifySSLHostnames ensures we verify the hostname on the certificate + // matches the host we are connecting and is signed + VerifySSLHostnames = SSLHostnameVerification(true) + // NoVerifySSLHostnames informs us to skip verifying the hostname + // matches a valid certificate + NoVerifySSLHostnames = SSLHostnameVerification(false) +) + +// GetHTTPClient returns either a standard http client or +// non validating client depending on the value of verify. +func GetHTTPClient(verify SSLHostnameVerification) *http.Client { + if verify == VerifySSLHostnames { + return GetValidatingHTTPClient() + } + return GetNonValidatingHTTPClient() +} + +// GetValidatingHTTPClient returns a new http.Client that +// verifies the server's certificate chain and hostname. +func GetValidatingHTTPClient() *http.Client { + logger.Infof("hostname SSL verification enabled") + return http.DefaultClient +} + +// GetNonValidatingHTTPClient returns a new http.Client that +// does not verify the server's certificate chain and hostname. func GetNonValidatingHTTPClient() *http.Client { + logger.Infof("hostname SSL verification disabled") insecureClientMutex.Lock() defer insecureClientMutex.Unlock() if insecureClient == nil { @@ -29,20 +76,10 @@ // See https://code.google.com/p/go/issues/detail?id=4677 // We need to force the connection to close each time so that we don't // hit the above Go bug. - return &http.Transport{ + transport := &http.Transport{ TLSClientConfig: tlsConfig, DisableKeepAlives: true, } -} - -// NewHttpTransport returns a new http.Transport constructed with the necessary -// parameters for Juju. -func NewHttpTransport() *http.Transport { - // See https://code.google.com/p/go/issues/detail?id=4677 - // We need to force the connection to close each time so that we don't - // hit the above Go bug. - return &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DisableKeepAlives: true, - } + registerFileProtocol(transport) + return transport } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/utils/http_test.go juju-core-1.17.7/src/launchpad.net/juju-core/utils/http_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/utils/http_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/utils/http_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -14,11 +14,11 @@ "launchpad.net/juju-core/utils" ) -type insecureClientSuite struct { +type httpSuite struct { Server *httptest.Server } -var _ = gc.Suite(&insecureClientSuite{}) +var _ = gc.Suite(&httpSuite{}) type trivialResponseHandler struct{} @@ -26,28 +26,46 @@ fmt.Fprintf(w, "Greetings!\n") } -func (s *insecureClientSuite) SetUpSuite(c *gc.C) { +func (s *httpSuite) SetUpSuite(c *gc.C) { } -func (s *insecureClientSuite) TearDownSuite(c *gc.C) { +func (s *httpSuite) TearDownSuite(c *gc.C) { } -func (s *insecureClientSuite) SetUpTest(c *gc.C) { +func (s *httpSuite) SetUpTest(c *gc.C) { s.Server = httptest.NewTLSServer(&trivialResponseHandler{}) } -func (s *insecureClientSuite) TearDownTest(c *gc.C) { +func (s *httpSuite) TearDownTest(c *gc.C) { if s.Server != nil { s.Server.Close() } } -func (s *insecureClientSuite) TestDefaultClientFails(c *gc.C) { +func (s *httpSuite) TestDefaultClientFails(c *gc.C) { _, err := http.Get(s.Server.URL) c.Assert(err, gc.ErrorMatches, "(.|\n)*x509: certificate signed by unknown authority") } -func (s *insecureClientSuite) TestInsecureClientSucceeds(c *gc.C) { +func (s *httpSuite) TestValidatingClientGetter(c *gc.C) { + client1 := utils.GetValidatingHTTPClient() + client2 := utils.GetHTTPClient(utils.VerifySSLHostnames) + c.Check(client1, gc.Equals, client2) +} + +func (s *httpSuite) TestNonValidatingClientGetter(c *gc.C) { + client1 := utils.GetNonValidatingHTTPClient() + client2 := utils.GetHTTPClient(utils.NoVerifySSLHostnames) + c.Check(client1, gc.Equals, client2) +} + +func (s *httpSuite) TestValidatingClientFails(c *gc.C) { + client := utils.GetValidatingHTTPClient() + _, err := client.Get(s.Server.URL) + c.Assert(err, gc.ErrorMatches, "(.|\n)*x509: certificate signed by unknown authority") +} + +func (s *httpSuite) TestInsecureClientSucceeds(c *gc.C) { response, err := utils.GetNonValidatingHTTPClient().Get(s.Server.URL) c.Assert(err, gc.IsNil) defer response.Body.Close() @@ -56,7 +74,7 @@ c.Check(string(body), gc.Equals, "Greetings!\n") } -func (s *insecureClientSuite) TestInsecureClientCached(c *gc.C) { +func (s *httpSuite) TestInsecureClientCached(c *gc.C) { client1 := utils.GetNonValidatingHTTPClient() client2 := utils.GetNonValidatingHTTPClient() c.Check(client1, gc.Equals, client2) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/utils/ssh/ssh_openssh.go juju-core-1.17.7/src/launchpad.net/juju-core/utils/ssh/ssh_openssh.go --- juju-core-1.17.6/src/launchpad.net/juju-core/utils/ssh/ssh_openssh.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/utils/ssh/ssh_openssh.go 2014-03-27 15:48:42.000000000 +0000 @@ -14,7 +14,7 @@ "launchpad.net/juju-core/utils" ) -var opensshCommonOptions = map[string][]string{"-o": []string{"StrictHostKeyChecking no"}} +var opensshCommonOptions = []string{"-o", "StrictHostKeyChecking no"} // default identities will not be attempted if // -i is specified and they are not explcitly @@ -63,22 +63,19 @@ return &c, nil } -func opensshOptions(options *Options, commandKind opensshCommandKind) map[string][]string { - args := make(map[string][]string) - for k, v := range opensshCommonOptions { - args[k] = v - } +func opensshOptions(options *Options, commandKind opensshCommandKind) []string { + args := append([]string{}, opensshCommonOptions...) if options == nil { options = &Options{} } if len(options.proxyCommand) > 0 { - args["-o"] = append(args["-o"], "ProxyCommand "+utils.CommandString(options.proxyCommand...)) + args = append(args, "-o", "ProxyCommand "+utils.CommandString(options.proxyCommand...)) } if !options.passwordAuthAllowed { - args["-o"] = append(args["-o"], "PasswordAuthentication no") + args = append(args, "-o", "PasswordAuthentication no") } if options.allocatePTY { - args["-t"] = []string{} + args = append(args, "-t", "-t") // twice to force } identities := append([]string{}, options.identities...) if pk := PrivateKeyFiles(); len(pk) > 0 { @@ -100,54 +97,29 @@ } } for _, identity := range identities { - args["-i"] = append(args["-i"], identity) + args = append(args, "-i", identity) } if options.port != 0 { port := fmt.Sprint(options.port) if commandKind == scpKind { // scp uses -P instead of -p (-p means preserve). - args["-P"] = []string{port} + args = append(args, "-P", port) } else { - args["-p"] = []string{port} + args = append(args, "-p", port) } } return args } -func expandArgs(args map[string][]string, quote bool) []string { - var list []string - for opt, vals := range args { - if len(vals) == 0 { - list = append(list, opt) - if opt == "-t" { - // In order to force a PTY to be allocated, we need to - // pass -t twice. - list = append(list, opt) - } - } - for _, val := range vals { - list = append(list, opt) - if quote { - val = fmt.Sprintf("%q", val) - } - list = append(list, val) - } - } - return list -} - // Command implements Client.Command. func (c *OpenSSHClient) Command(host string, command []string, options *Options) *Cmd { - opts := opensshOptions(options, sshKind) - args := expandArgs(opts, false) + args := opensshOptions(options, sshKind) args = append(args, host) if len(command) > 0 { args = append(args, command...) } bin, args := sshpassWrap("ssh", args) - optsList := strings.Join(expandArgs(opts, true), " ") - fullCommand := strings.Join(command, " ") - logger.Debugf("running: %s %s %q '%s'", bin, optsList, host, fullCommand) + logger.Debugf("running: %s %s", bin, utils.CommandString(args...)) return &Cmd{impl: &opensshCmd{exec.Command(bin, args...)}} } @@ -158,18 +130,14 @@ options = *userOptions options.allocatePTY = false // doesn't make sense for scp } - opts := opensshOptions(&options, scpKind) - args := expandArgs(opts, false) + args := opensshOptions(&options, scpKind) args = append(args, extraArgs...) args = append(args, targets...) bin, args := sshpassWrap("scp", args) cmd := exec.Command(bin, args...) var stderr bytes.Buffer cmd.Stderr = &stderr - allOpts := append(expandArgs(opts, true), extraArgs...) - optsList := strings.Join(allOpts, " ") - targetList := `"` + strings.Join(targets, `" "`) + `"` - logger.Debugf("running: %s %s %s", bin, optsList, targetList) + logger.Debugf("running: %s %s", bin, utils.CommandString(args...)) if err := cmd.Run(); err != nil { stderr := strings.TrimSpace(stderr.String()) if len(stderr) > 0 { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/version/version.go juju-core-1.17.7/src/launchpad.net/juju-core/version/version.go --- juju-core-1.17.6/src/launchpad.net/juju-core/version/version.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/version/version.go 2014-03-27 15:48:42.000000000 +0000 @@ -23,7 +23,7 @@ // The presence and format of this constant is very important. // The debian/rules build recipe uses this value for the version // number of the release package. -const version = "1.17.6" +const version = "1.17.7" // lsbReleaseFile is the name of the file that is read in order to determine // the release version of ubuntu. diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/environ.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/environ.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/environ.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/environ.go 2014-03-27 15:48:42.000000000 +0000 @@ -5,12 +5,15 @@ import ( "errors" + "fmt" + "sync" "github.com/juju/loggo" "launchpad.net/tomb" "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/config" + "launchpad.net/juju-core/state" apiwatcher "launchpad.net/juju-core/state/api/watcher" "launchpad.net/juju-core/state/watcher" ) @@ -27,6 +30,9 @@ EnvironConfig() (*config.Config, error) } +// TODO(rog) remove WaitForEnviron, as we now should always +// start with a valid environ config. + // WaitForEnviron waits for an valid environment to arrive from // the given watcher. It terminates with tomb.ErrDying if // it receives a value on dying. @@ -52,3 +58,82 @@ } } } + +// EnvironObserver watches the current environment configuration +// and makes it available. It discards invalid environment +// configurations. +type EnvironObserver struct { + tomb tomb.Tomb + environWatcher state.NotifyWatcher + st *state.State + mu sync.Mutex + environ environs.Environ +} + +// NewEnvironObserver waits for the state to have a valid environment +// configuration and returns a new environment observer. While waiting +// for the first environment configuration, it will return with +// tomb.ErrDying if it receives a value on dying. +func NewEnvironObserver(st *state.State) (*EnvironObserver, error) { + config, err := st.EnvironConfig() + if err != nil { + return nil, err + } + environ, err := environs.New(config) + if err != nil { + return nil, fmt.Errorf("cannot make Environ: %v", err) + } + environWatcher := st.WatchForEnvironConfigChanges() + obs := &EnvironObserver{ + st: st, + environ: environ, + environWatcher: environWatcher, + } + go func() { + defer obs.tomb.Done() + defer watcher.Stop(environWatcher, &obs.tomb) + obs.tomb.Kill(obs.loop()) + }() + return obs, nil +} + +func (obs *EnvironObserver) loop() error { + for { + select { + case <-obs.tomb.Dying(): + return nil + case _, ok := <-obs.environWatcher.Changes(): + if !ok { + return watcher.MustErr(obs.environWatcher) + } + } + config, err := obs.st.EnvironConfig() + if err != nil { + logger.Warningf("error reading environment config: %v", err) + continue + } + environ, err := environs.New(config) + if err != nil { + logger.Warningf("error creating Environ: %v", err) + continue + } + obs.mu.Lock() + obs.environ = environ + obs.mu.Unlock() + } +} + +// Environ returns the most recent valid Environ. +func (obs *EnvironObserver) Environ() environs.Environ { + obs.mu.Lock() + defer obs.mu.Unlock() + return obs.environ +} + +func (obs *EnvironObserver) Kill() { + obs.tomb.Kill(nil) +} + +func (obs *EnvironObserver) Wait() error { + return obs.tomb.Wait() +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/environ_test.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/environ_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/environ_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/environ_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -4,8 +4,11 @@ package worker_test import ( + "strings" stdtesting "testing" + "time" + "github.com/juju/loggo" gc "launchpad.net/gocheck" "launchpad.net/tomb" @@ -16,17 +19,17 @@ "launchpad.net/juju-core/worker" ) -type waitForEnvironSuite struct { - testing.JujuConnSuite -} - -var _ = gc.Suite(&waitForEnvironSuite{}) - func TestPackage(t *stdtesting.T) { coretesting.MgoTestPackage(t) } -func (s *waitForEnvironSuite) TestStop(c *gc.C) { +type environSuite struct { + testing.JujuConnSuite +} + +var _ = gc.Suite(&environSuite{}) + +func (s *environSuite) TestStop(c *gc.C) { w := s.State.WatchForEnvironConfigChanges() defer stopWatcher(c, w) stop := make(chan struct{}) @@ -45,7 +48,7 @@ c.Check(err, gc.IsNil) } -func (s *waitForEnvironSuite) TestInvalidConfig(c *gc.C) { +func (s *environSuite) TestInvalidConfig(c *gc.C) { var oldType string oldType = s.Conn.Environ.Config().AllAttrs()["type"].(string) @@ -79,3 +82,80 @@ c.Assert(env, gc.NotNil) c.Assert(env.Config().AllAttrs()["secret"], gc.Equals, "environ_test") } + +func (s *environSuite) TestErrorWhenEnvironIsInvalid(c *gc.C) { + // reopen the state so that we can wangle a dodgy environ config in there. + st, err := state.Open(s.StateInfo(c), state.DefaultDialOpts(), state.Policy(nil)) + c.Assert(err, gc.IsNil) + defer st.Close() + err = st.UpdateEnvironConfig(map[string]interface{}{"secret": 999}, nil, nil) + c.Assert(err, gc.IsNil) + obs, err := worker.NewEnvironObserver(s.State) + c.Assert(err, gc.ErrorMatches, `cannot make Environ: secret: expected string, got int\(999\)`) + c.Assert(obs, gc.IsNil) +} + +func (s *environSuite) TestEnvironmentChanges(c *gc.C) { + originalConfig, err := s.State.EnvironConfig() + c.Assert(err, gc.IsNil) + + logc := make(logChan, 1009) + c.Assert(loggo.RegisterWriter("testing", logc, loggo.WARNING), gc.IsNil) + defer loggo.RemoveWriter("testing") + + obs, err := worker.NewEnvironObserver(s.State) + c.Assert(err, gc.IsNil) + + env := obs.Environ() + c.Assert(env.Config().AllAttrs(), gc.DeepEquals, originalConfig.AllAttrs()) + var oldType string + oldType = env.Config().AllAttrs()["type"].(string) + + info := s.StateInfo(c) + opts := state.DefaultDialOpts() + st2, err := state.Open(info, opts, state.Policy(nil)) + defer st2.Close() + + // Change to an invalid configuration and check + // that the observer's environment remains the same. + st2.UpdateEnvironConfig(map[string]interface{}{"type": "invalid"}, nil, nil) + st2.StartSync() + + // Wait for the observer to register the invalid environment + timeout := time.After(coretesting.LongWait) +loop: + for { + select { + case msg := <-logc: + if strings.Contains(msg, "error creating Environ") { + break loop + } + case <-timeout: + c.Fatalf("timed out waiting to see broken environment") + } + } + // Check that the returned environ is still the same. + env = obs.Environ() + c.Assert(env.Config().AllAttrs(), gc.DeepEquals, originalConfig.AllAttrs()) + + // Change the environment back to a valid configuration + // with a different name and check that we see it. + st2.UpdateEnvironConfig(map[string]interface{}{"type": oldType, "name": "a-new-name"}, nil, nil) + st2.StartSync() + + for a := coretesting.LongAttempt.Start(); a.Next(); { + env := obs.Environ() + if !a.HasNext() { + c.Fatalf("timed out waiting for new environ") + } + if env.Config().Name() == "a-new-name" { + break + } + } +} + +type logChan chan string + +func (logc logChan) Write(level loggo.Level, name, filename string, line int, timestamp time.Time, message string) { + logc <- message +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/instancepoller/observer.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/instancepoller/observer.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/instancepoller/observer.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/instancepoller/observer.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,94 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package instancepoller - -import ( - "sync" - - "launchpad.net/tomb" - - "launchpad.net/juju-core/environs" - "launchpad.net/juju-core/state" - "launchpad.net/juju-core/state/watcher" - "launchpad.net/juju-core/worker" -) - -// TODO(rog) 2013-10-02 -// Put this somewhere generally available and -// refactor other workers to use it. - -// environObserver watches the current environment configuration -// and makes it available. It discards invalid environment -// configurations. -type environObserver struct { - tomb tomb.Tomb - environWatcher state.NotifyWatcher - st *state.State - mu sync.Mutex - environ environs.Environ -} - -// newEnvironObserver waits for the state to have a valid environment -// configuration and returns a new environment observer. While waiting -// for the first environment configuration, it will return with -// tomb.ErrDying if it receives a value on dying. -func newEnvironObserver(st *state.State, dying <-chan struct{}) (*environObserver, error) { - environWatcher := st.WatchForEnvironConfigChanges() - environ, err := worker.WaitForEnviron(environWatcher, st, dying) - if err != nil { - return nil, err - } - obs := &environObserver{ - st: st, - environ: environ, - environWatcher: environWatcher, - } - go func() { - defer obs.tomb.Done() - defer watcher.Stop(environWatcher, &obs.tomb) - obs.tomb.Kill(obs.loop()) - }() - return obs, nil -} - -func (obs *environObserver) loop() error { - for { - select { - case <-obs.tomb.Dying(): - return nil - case _, ok := <-obs.environWatcher.Changes(): - if !ok { - return watcher.MustErr(obs.environWatcher) - } - } - config, err := obs.st.EnvironConfig() - if err != nil { - logger.Warningf("error reading environment config: %v", err) - continue - } - environ, err := environs.New(config) - if err != nil { - logger.Warningf("error creating Environ: %v", err) - continue - } - obs.mu.Lock() - obs.environ = environ - obs.mu.Unlock() - } -} - -// Environ returns the most recent valid Environ. -func (obs *environObserver) Environ() environs.Environ { - obs.mu.Lock() - defer obs.mu.Unlock() - return obs.environ -} - -func (obs *environObserver) Kill() { - obs.tomb.Kill(nil) -} - -func (obs *environObserver) Wait() error { - return obs.tomb.Wait() -} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/instancepoller/observer_test.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/instancepoller/observer_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/instancepoller/observer_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/instancepoller/observer_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,97 +0,0 @@ -// Copyright 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -// TODO(wallyworld) - move to instancepoller_test -package instancepoller - -import ( - "strings" - "time" - - "github.com/juju/loggo" - gc "launchpad.net/gocheck" - - "launchpad.net/juju-core/juju/testing" - "launchpad.net/juju-core/state" - coretesting "launchpad.net/juju-core/testing" -) - -var _ = gc.Suite(&observerSuite{}) - -type observerSuite struct { - testing.JujuConnSuite -} - -func (s *observerSuite) TestWaitsForValidEnviron(c *gc.C) { - obs, err := newEnvironObserver(s.State, nil) - c.Assert(err, gc.IsNil) - env := obs.Environ() - stateConfig, err := s.State.EnvironConfig() - c.Assert(err, gc.IsNil) - c.Assert(env.Config().AllAttrs(), gc.DeepEquals, stateConfig.AllAttrs()) -} - -func (s *observerSuite) TestEnvironmentChanges(c *gc.C) { - originalConfig, err := s.State.EnvironConfig() - c.Assert(err, gc.IsNil) - - logc := make(logChan, 1009) - c.Assert(loggo.RegisterWriter("testing", logc, loggo.WARNING), gc.IsNil) - defer loggo.RemoveWriter("testing") - - obs, err := newEnvironObserver(s.State, nil) - c.Assert(err, gc.IsNil) - - env := obs.Environ() - c.Assert(env.Config().AllAttrs(), gc.DeepEquals, originalConfig.AllAttrs()) - var oldType string - oldType = env.Config().AllAttrs()["type"].(string) - - info := s.StateInfo(c) - opts := state.DefaultDialOpts() - st2, err := state.Open(info, opts, state.Policy(nil)) - defer st2.Close() - - // Change to an invalid configuration and check - // that the observer's environment remains the same. - st2.UpdateEnvironConfig(map[string]interface{}{"type": "invalid"}, nil, nil) - st2.StartSync() - - // Wait for the observer to register the invalid environment - timeout := time.After(coretesting.LongWait) -loop: - for { - select { - case msg := <-logc: - if strings.Contains(msg, "error creating Environ") { - break loop - } - case <-timeout: - c.Fatalf("timed out waiting to see broken environment") - } - } - // Check that the returned environ is still the same. - env = obs.Environ() - c.Assert(env.Config().AllAttrs(), gc.DeepEquals, originalConfig.AllAttrs()) - - // Change the environment back to a valid configuration - // with a different name and check that we see it. - st2.UpdateEnvironConfig(map[string]interface{}{"type": oldType, "name": "a-new-name"}, nil, nil) - st2.StartSync() - - for a := coretesting.LongAttempt.Start(); a.Next(); { - env := obs.Environ() - if !a.HasNext() { - c.Fatalf("timed out waiting for new environ") - } - if env.Config().Name() == "a-new-name" { - break - } - } -} - -type logChan chan string - -func (logc logChan) Write(level loggo.Level, name, filename string, line int, timestamp time.Time, message string) { - logc <- message -} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/instancepoller/worker.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/instancepoller/worker.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/instancepoller/worker.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/instancepoller/worker.go 2014-03-27 15:48:42.000000000 +0000 @@ -15,7 +15,7 @@ tomb tomb.Tomb *aggregator - observer *environObserver + observer *worker.EnvironObserver } // NewWorker returns a worker that keeps track of @@ -42,11 +42,11 @@ } func (u *updaterWorker) loop() (err error) { - u.observer, err = newEnvironObserver(u.st, u.tomb.Dying()) + u.observer, err = worker.NewEnvironObserver(u.st) if err != nil { return err } - u.aggregator = newAggregator(u.observer.environ) + u.aggregator = newAggregator(u.observer.Environ()) logger.Infof("instance poller received inital environment configuration") defer func() { obsErr := worker.Stop(u.observer) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/peergrouper/desired.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/peergrouper/desired.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/peergrouper/desired.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/peergrouper/desired.go 2014-03-27 15:48:42.000000000 +0000 @@ -149,12 +149,13 @@ changed := false // Make sure all members' machine addresses are up to date. for _, m := range machines { - if m.hostPort == "" { + hp := m.mongoHostPort() + if hp == "" { continue } // TODO ensure that replicaset works correctly with IPv6 [host]:port addresses. - if m.hostPort != members[m].Address { - members[m].Address = m.hostPort + if hp != members[m].Address { + members[m].Address = hp changed = true } } @@ -206,7 +207,8 @@ setVoting func(*machine, bool), ) { for _, m := range toKeep { - if members[m] == nil && m.hostPort != "" { + hasAddress := m.mongoHostPort() != "" + if members[m] == nil && hasAddress { // This machine was not previously in the members list, // so add it (as non-voting). We maintain the // id manually to make it easier for tests. @@ -219,7 +221,7 @@ } members[m] = member setVoting(m, false) - } else if m.hostPort == "" { + } else if !hasAddress { logger.Debugf("ignoring machine %q with no address", m.id) } } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/peergrouper/desired_test.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/peergrouper/desired_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/peergrouper/desired_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/peergrouper/desired_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -13,6 +13,7 @@ jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" + "launchpad.net/juju-core/instance" "launchpad.net/juju-core/replicaset" coretesting "launchpad.net/juju-core/testing" "launchpad.net/juju-core/testing/testbase" @@ -28,7 +29,10 @@ var _ = gc.Suite(&desiredPeerGroupSuite{}) -const mongoPort = 1234 +const ( + mongoPort = 1234 + apiPort = 5678 +) var desiredPeerGroupTests = []struct { about string @@ -153,7 +157,14 @@ machines: append(mkMachines("11v 12v"), &machine{ id: "13", wantsVote: true, - hostPort: "0.1.99.13:1234", + mongoHostPorts: []instance.HostPort{{ + Address: instance.Address{ + Value: "0.1.99.13", + Type: instance.Ipv4Address, + NetworkScope: instance.NetworkCloudLocal, + }, + Port: 1234, + }}, }), statuses: mkStatuses("1s 2p 3p"), members: mkMembers("1v 2v 3v"), @@ -166,9 +177,9 @@ }, { about: "a machine's address is ignored if it changes to empty", machines: append(mkMachines("11v 12v"), &machine{ - id: "13", - wantsVote: true, - hostPort: "", + id: "13", + wantsVote: true, + mongoHostPorts: nil, }), statuses: mkStatuses("1s 2p 3p"), members: mkMembers("1v 2v 3v"), @@ -248,8 +259,15 @@ ms := make([]*machine, len(descrs)) for i, d := range descrs { ms[i] = &machine{ - id: fmt.Sprint(d.id), - hostPort: fmt.Sprintf("0.1.2.%d:%d", d.id, mongoPort), + id: fmt.Sprint(d.id), + mongoHostPorts: []instance.HostPort{{ + Address: instance.Address{ + Value: fmt.Sprintf("0.1.2.%d", d.id), + Type: instance.Ipv4Address, + NetworkScope: instance.NetworkCloudLocal, + }, + Port: mongoPort, + }}, wantsVote: strings.Contains(d.flags, "v"), } } @@ -386,6 +404,13 @@ return descrs } +func assertMembers(c *gc.C, obtained interface{}, expected []replicaset.Member) { + c.Assert(obtained, gc.FitsTypeOf, []replicaset.Member{}) + sort.Sort(membersById(obtained.([]replicaset.Member))) + sort.Sort(membersById(expected)) + c.Assert(obtained, jc.DeepEquals, expected) +} + type membersById []replicaset.Member func (l membersById) Len() int { return len(l) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/peergrouper/mock_test.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/peergrouper/mock_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/peergrouper/mock_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/peergrouper/mock_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -6,13 +6,16 @@ import ( "encoding/json" "fmt" + "net" "path" "reflect" + "strconv" "sync" "launchpad.net/tomb" "launchpad.net/juju-core/errors" + "launchpad.net/juju-core/instance" "launchpad.net/juju-core/replicaset" "launchpad.net/juju-core/state" "launchpad.net/juju-core/utils/voyeur" @@ -233,6 +236,15 @@ checker invariantChecker } +type machineDoc struct { + id string + wantsVote bool + hasVote bool + instanceId instance.Id + mongoHostPorts []instance.HostPort + apiHostPorts []instance.HostPort +} + func (m *fakeMachine) Refresh() error { if err := errorFor("Machine.Refresh", m.doc.id); err != nil { return err @@ -249,6 +261,13 @@ return m.doc.id } +func (m *fakeMachine) InstanceId() (instance.Id, error) { + if err := errorFor("Machine.InstanceId", m.doc.id); err != nil { + return "", err + } + return m.doc.instanceId, nil +} + func (m *fakeMachine) Watch() state.NotifyWatcher { return WatchValue(&m.val) } @@ -261,8 +280,12 @@ return m.doc.hasVote } -func (m *fakeMachine) StateHostPort() string { - return m.doc.hostPort +func (m *fakeMachine) MongoHostPorts() []instance.HostPort { + return m.doc.mongoHostPorts +} + +func (m *fakeMachine) APIHostPorts() []instance.HostPort { + return m.doc.apiHostPorts } // mutate atomically changes the machineDoc of @@ -278,8 +301,40 @@ } func (m *fakeMachine) setStateHostPort(hostPort string) { + var mongoHostPorts []instance.HostPort + if hostPort != "" { + host, portStr, err := net.SplitHostPort(hostPort) + if err != nil { + panic(err) + } + port, err := strconv.Atoi(portStr) + if err != nil { + panic(err) + } + mongoHostPorts = instance.AddressesWithPort(instance.NewAddresses([]string{host}), port) + mongoHostPorts[0].NetworkScope = instance.NetworkCloudLocal + } + m.mutate(func(doc *machineDoc) { - doc.hostPort = hostPort + doc.mongoHostPorts = mongoHostPorts + }) +} + +func (m *fakeMachine) setMongoHostPorts(hostPorts []instance.HostPort) { + m.mutate(func(doc *machineDoc) { + doc.mongoHostPorts = hostPorts + }) +} + +func (m *fakeMachine) setAPIHostPorts(hostPorts []instance.HostPort) { + m.mutate(func(doc *machineDoc) { + doc.apiHostPorts = hostPorts + }) +} + +func (m *fakeMachine) setInstanceId(instanceId instance.Id) { + m.mutate(func(doc *machineDoc) { + doc.instanceId = instanceId }) } @@ -300,13 +355,6 @@ }) } -type machineDoc struct { - id string - wantsVote bool - hasVote bool - hostPort string -} - type fakeMongoSession struct { // If InstantlyReady is true, replica status of // all members will be instantly reported as ready. diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/peergrouper/publish.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/peergrouper/publish.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/peergrouper/publish.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/peergrouper/publish.go 2014-03-27 15:48:42.000000000 +0000 @@ -0,0 +1,24 @@ +// Copyright 2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package peergrouper + +import ( + "launchpad.net/juju-core/instance" + "launchpad.net/juju-core/state" +) + +type publisher struct { + st *state.State +} + +func newPublisher(st *state.State) *publisher { + return &publisher{ + st: st, + } +} + +func (pub *publisher) publishAPIServers(apiServers [][]instance.HostPort, instanceIds []instance.Id) error { + // TODO(rog) publish instanceIds in environment storage. + return pub.st.SetAPIHostPorts(apiServers) +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/peergrouper/shim.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/peergrouper/shim.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/peergrouper/shim.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/peergrouper/shim.go 2014-03-27 15:48:42.000000000 +0000 @@ -4,9 +4,6 @@ package peergrouper import ( - "net" - "strconv" - "labix.org/v2/mgo" "launchpad.net/juju-core/instance" @@ -21,6 +18,7 @@ type stateShim struct { *state.State mongoPort int + apiPort int } func (s *stateShim) Machine(id string) (stateMachine, error) { @@ -31,6 +29,7 @@ return &machineShim{ Machine: m, mongoPort: s.mongoPort, + apiPort: s.apiPort, }, nil } @@ -38,17 +37,18 @@ return mongoSessionShim{s.State.MongoSession()} } -func (m *machineShim) StateHostPort() string { - privateAddr := instance.SelectInternalAddress(m.Addresses(), false) - if privateAddr == "" { - return "" - } - return net.JoinHostPort(privateAddr, strconv.Itoa(m.mongoPort)) +func (m *machineShim) APIHostPorts() []instance.HostPort { + return instance.AddressesWithPort(m.Addresses(), m.apiPort) +} + +func (m *machineShim) MongoHostPorts() []instance.HostPort { + return instance.AddressesWithPort(m.Addresses(), m.mongoPort) } type machineShim struct { *state.Machine mongoPort int + apiPort int } type mongoSessionShim struct { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/peergrouper/worker.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/peergrouper/worker.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/peergrouper/worker.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/peergrouper/worker.go 2014-03-27 15:48:42.000000000 +0000 @@ -11,6 +11,7 @@ "launchpad.net/tomb" "launchpad.net/juju-core/errors" + "launchpad.net/juju-core/instance" "launchpad.net/juju-core/replicaset" "launchpad.net/juju-core/state" "launchpad.net/juju-core/worker" @@ -25,12 +26,14 @@ type stateMachine interface { Id() string + InstanceId() (instance.Id, error) Refresh() error Watch() state.NotifyWatcher WantsVote() bool HasVote() bool SetHasVote(hasVote bool) error - StateHostPort() string + APIHostPorts() []instance.HostPort + MongoHostPorts() []instance.HostPort } type mongoSession interface { @@ -39,12 +42,19 @@ Set([]replicaset.Member) error } +type publisherInterface interface { + // publish publishes information about the given state servers + // to whomsoever it may concern. When it is called there + // is no guarantee that any of the information has actually changed. + publishAPIServers(apiServers [][]instance.HostPort, instanceIds []instance.Id) error +} + // notifyFunc holds a function that is sent // to the main worker loop to fetch new information // when something changes. It reports whether // the information has actually changed (and by implication // whether the replica set may need to be changed). -type notifyFunc func() (bool, error) +type notifyFunc func() (changed bool, err error) var ( // If we fail to set the mongo replica set members, @@ -74,11 +84,11 @@ // before finishing. wg sync.WaitGroup - // st represents the State. It is an interface for testing - // purposes only. + // st represents the State. It is an interface so we can swap + // out the implementation during testing. st stateInterface - // When something changes that might might affect + // When something changes that might affect // the peer group membership, it sends a function // on notifyCh that is run inside the main worker // goroutine to mutate the state. It reports whether @@ -90,6 +100,10 @@ // associated goroutine that // watches attributes of that machine. machines map[string]*machine + + // publisher holds the implementation of the API + // address publisher. + publisher publisherInterface } // New returns a new worker that maintains the mongo replica set @@ -102,15 +116,18 @@ return newWorker(&stateShim{ State: st, mongoPort: cfg.StatePort(), - }), nil + apiPort: cfg.APIPort(), + }, newPublisher(st)), nil } -func newWorker(st stateInterface) worker.Worker { +func newWorker(st stateInterface, pub publisherInterface) worker.Worker { w := &pgWorker{ - st: st, - notifyCh: make(chan notifyFunc), - machines: make(map[string]*machine), + st: st, + notifyCh: make(chan notifyFunc), + machines: make(map[string]*machine), + publisher: pub, } + logger.Infof("worker starting") go func() { defer w.tomb.Done() if err := w.loop(); err != nil { @@ -154,25 +171,55 @@ // Try to update the replica set immediately. retry.Reset(0) case <-retry.C: + ok := true + servers, instanceIds, err := w.apiPublishInfo() + if err != nil { + return fmt.Errorf("cannot get API server info: %v", err) + } + if err := w.publisher.publishAPIServers(servers, instanceIds); err != nil { + logger.Errorf("cannot publish API server addresses: %v", err) + ok = false + } if err := w.updateReplicaset(); err != nil { if _, isReplicaSetError := err.(*replicaSetError); !isReplicaSetError { return err } logger.Errorf("cannot set replicaset: %v", err) + ok = false + } + if ok { + // Update the replica set members occasionally + // to keep them up to date with the current + // replica set member statuses. + retry.Reset(pollInterval) + } else { retry.Reset(retryInterval) - break } - // Update the replica set members occasionally - // to keep them up to date with the current - // replica set member statuses. - retry.Reset(pollInterval) case <-w.tomb.Dying(): return tomb.ErrDying } } } +func (w *pgWorker) apiPublishInfo() ([][]instance.HostPort, []instance.Id, error) { + servers := make([][]instance.HostPort, 0, len(w.machines)) + instanceIds := make([]instance.Id, 0, len(w.machines)) + for _, m := range w.machines { + if len(m.apiHostPorts) == 0 { + continue + } + instanceId, err := m.stm.InstanceId() + if err != nil { + return nil, nil, err + } + instanceIds = append(instanceIds, instanceId) + servers = append(servers, m.apiHostPorts) + + } + return servers, instanceIds, nil +} + // notify sends the given notification function to // the worker main loop to be executed. func (w *pgWorker) notify(f notifyFunc) bool { @@ -184,7 +231,7 @@ } } -// getPeerGroupInfo collates current session information about the +// peerGroupInfo collates current session information about the // mongo peer group with information from state machines. func (w *pgWorker) peerGroupInfo() (*peerGroupInfo, error) { session := w.st.MongoSession() @@ -289,7 +336,6 @@ // setHasVote sets the HasVote status of all the given // machines to hasVote. func setHasVote(ms []*machine, hasVote bool) error { - for _, m := range ms { if err := m.stm.SetHasVote(hasVote); err != nil { return fmt.Errorf("cannot set voting status of %q to %v: %v", m.id, hasVote, err) @@ -375,21 +421,26 @@ // machine represents a machine in State. type machine struct { - id string - wantsVote bool - hostPort string + id string + wantsVote bool + apiHostPorts []instance.HostPort + mongoHostPorts []instance.HostPort worker *pgWorker stm stateMachine machineWatcher state.NotifyWatcher } +func (m *machine) mongoHostPort() string { + return instance.SelectInternalHostPort(m.mongoHostPorts, false) +} + func (m *machine) String() string { return m.id } func (m *machine) GoString() string { - return fmt.Sprintf("&peergrouper.machine{id: %q, wantsVote: %v, hostPort: %q}", m.id, m.wantsVote, m.hostPort) + return fmt.Sprintf("&peergrouper.machine{id: %q, wantsVote: %v, hostPort: %q}", m.id, m.wantsVote, m.mongoHostPort()) } func (w *pgWorker) newMachine(stm stateMachine) *machine { @@ -397,7 +448,8 @@ worker: w, id: stm.Id(), stm: stm, - hostPort: stm.StateHostPort(), + apiHostPorts: stm.APIHostPorts(), + mongoHostPorts: stm.MongoHostPorts(), wantsVote: stm.WantsVote(), machineWatcher: stm.Watch(), } @@ -441,13 +493,29 @@ m.wantsVote = wantsVote changed = true } - if hostPort := m.stm.StateHostPort(); hostPort != m.hostPort { - m.hostPort = hostPort + if hps := m.stm.MongoHostPorts(); !hostPortsEqual(hps, m.mongoHostPorts) { + m.mongoHostPorts = hps + changed = true + } + if hps := m.stm.APIHostPorts(); !hostPortsEqual(hps, m.apiHostPorts) { + m.apiHostPorts = hps changed = true } return changed, nil } +func hostPortsEqual(hps1, hps2 []instance.HostPort) bool { + if len(hps1) != len(hps2) { + return false + } + for i := range hps1 { + if hps1[i] != hps2[i] { + return false + } + } + return true +} + func inStrings(t string, ss []string) bool { for _, s := range ss { if s == t { diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/peergrouper/worker_test.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/peergrouper/worker_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/peergrouper/worker_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/peergrouper/worker_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -11,7 +11,9 @@ jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" + "launchpad.net/juju-core/instance" "launchpad.net/juju-core/juju/testing" + statetesting "launchpad.net/juju-core/state/testing" coretesting "launchpad.net/juju-core/testing" "launchpad.net/juju-core/testing/testbase" "launchpad.net/juju-core/utils/voyeur" @@ -31,6 +33,35 @@ c.Assert(err, gc.IsNil) } +func (s *workerJujuConnSuite) TestPublisherSetsAPIHostPorts(c *gc.C) { + st := newFakeState() + initState(c, st, 3) + + watcher := s.State.WatchAPIHostPorts() + cwatch := statetesting.NewNotifyWatcherC(c, s.State, watcher) + cwatch.AssertOneChange() + + statePublish := newPublisher(s.State) + + // Wrap the publisher so that we can call StartSync immediately + // after the publishAPIServers method is called. + publish := func(apiServers [][]instance.HostPort, instanceIds []instance.Id) error { + err := statePublish.publishAPIServers(apiServers, instanceIds) + s.State.StartSync() + return err + } + + w := newWorker(st, publisherFunc(publish)) + defer func() { + c.Check(worker.Stop(w), gc.IsNil) + }() + + cwatch.AssertOneChange() + hps, err := s.State.APIHostPorts() + c.Assert(err, gc.IsNil) + c.Assert(hps, jc.DeepEquals, expectedAPIHostPorts(3)) +} + type workerSuite struct { testbase.LoggingSuite } @@ -50,8 +81,12 @@ for i := 10; i < 10+numMachines; i++ { id := fmt.Sprint(i) m := st.addMachine(id, true) + m.setInstanceId(instance.Id("id-" + id)) m.setStateHostPort(fmt.Sprintf("0.1.2.%d:%d", i, mongoPort)) ids = append(ids, id) + c.Assert(m.MongoHostPorts(), gc.HasLen, 1) + + m.setAPIHostPorts(addressesWithPort(apiPort, fmt.Sprintf("0.1.2.%d", i))) } st.machine("10").SetHasVote(true) st.setStateServers(ids...) @@ -60,6 +95,27 @@ st.check = checkInvariants } +// expectedAPIHostPorts returns the expected addresses +// of the machines as created by initState. +func expectedAPIHostPorts(n int) [][]instance.HostPort { + servers := make([][]instance.HostPort, n) + for i := range servers { + servers[i] = []instance.HostPort{{ + Address: instance.Address{ + Value: fmt.Sprintf("0.1.2.%d", i+10), + NetworkScope: instance.NetworkUnknown, + Type: instance.Ipv4Address, + }, + Port: apiPort, + }} + } + return servers +} + +func addressesWithPort(port int, addrs ...string) []instance.HostPort { + return instance.AddressesWithPort(instance.NewAddresses(addrs), port) +} + func (s *workerSuite) TestSetsAndUpdatesMembers(c *gc.C) { s.PatchValue(&pollInterval, 5*time.Millisecond) @@ -68,24 +124,24 @@ memberWatcher := st.session.members.Watch() mustNext(c, memberWatcher) - c.Assert(memberWatcher.Value(), jc.DeepEquals, mkMembers("0v")) + assertMembers(c, memberWatcher.Value(), mkMembers("0v")) logger.Infof("starting worker") - w := newWorker(st) + w := newWorker(st, noPublisher{}) defer func() { c.Check(worker.Stop(w), gc.IsNil) }() // Wait for the worker to set the initial members. mustNext(c, memberWatcher) - c.Assert(memberWatcher.Value(), jc.DeepEquals, mkMembers("0v 1 2")) + assertMembers(c, memberWatcher.Value(), mkMembers("0v 1 2")) // Update the status of the new members // and check that they become voting. c.Logf("updating new member status") st.session.setStatus(mkStatuses("0p 1s 2s")) mustNext(c, memberWatcher) - c.Assert(memberWatcher.Value(), jc.DeepEquals, mkMembers("0v 1v 2v")) + assertMembers(c, memberWatcher.Value(), mkMembers("0v 1v 2v")) c.Logf("adding another machine") // Add another machine. @@ -95,7 +151,7 @@ c.Logf("waiting for new member to be added") mustNext(c, memberWatcher) - c.Assert(memberWatcher.Value(), jc.DeepEquals, mkMembers("0v 1v 2v 3")) + assertMembers(c, memberWatcher.Value(), mkMembers("0v 1v 2v 3")) // Remove vote from an existing member; // and give it to the new machine. @@ -111,7 +167,7 @@ // old machine loses it. c.Logf("waiting for vote switch") mustNext(c, memberWatcher) - c.Assert(memberWatcher.Value(), jc.DeepEquals, mkMembers("0 1v 2v 3v")) + assertMembers(c, memberWatcher.Value(), mkMembers("0 1v 2v 3v")) c.Logf("removing old machine") // Remove the old machine. @@ -121,7 +177,7 @@ // Check that it's removed from the members. c.Logf("waiting for removal") mustNext(c, memberWatcher) - c.Assert(memberWatcher.Value(), jc.DeepEquals, mkMembers("1v 2v 3v")) + assertMembers(c, memberWatcher.Value(), mkMembers("1v 2v 3v")) } func (s *workerSuite) TestAddressChange(c *gc.C) { @@ -130,17 +186,17 @@ memberWatcher := st.session.members.Watch() mustNext(c, memberWatcher) - c.Assert(memberWatcher.Value(), jc.DeepEquals, mkMembers("0v")) + assertMembers(c, memberWatcher.Value(), mkMembers("0v")) logger.Infof("starting worker") - w := newWorker(st) + w := newWorker(st, noPublisher{}) defer func() { c.Check(worker.Stop(w), gc.IsNil) }() // Wait for the worker to set the initial members. mustNext(c, memberWatcher) - c.Assert(memberWatcher.Value(), jc.DeepEquals, mkMembers("0v 1 2")) + assertMembers(c, memberWatcher.Value(), mkMembers("0v 1 2")) // Change an address and wait for it to be changed in the // members. @@ -149,7 +205,7 @@ mustNext(c, memberWatcher) expectMembers := mkMembers("0v 1 2") expectMembers[1].Address = "0.1.99.99:9876" - c.Assert(memberWatcher.Value(), jc.DeepEquals, expectMembers) + assertMembers(c, memberWatcher.Value(), expectMembers) } var fatalErrorsTests = []struct { @@ -171,6 +227,9 @@ }, { errPattern: "State.Machine *", expectErr: `cannot get machine "10": sample`, +}, { + errPattern: "Machine.InstanceId *", + expectErr: `cannot get API server info: sample`, }} func (s *workerSuite) TestFatalErrors(c *gc.C) { @@ -182,7 +241,7 @@ st.session.InstantlyReady = true initState(c, st, 3) setErrorFor(test.errPattern, errors.New("sample")) - w := newWorker(st) + w := newWorker(st, noPublisher{}) done := make(chan error) go func() { done <- w.Wait() @@ -208,7 +267,7 @@ return errors.New("sample") }) s.PatchValue(&retryInterval, 5*time.Millisecond) - w := newWorker(st) + w := newWorker(st, noPublisher{}) defer func() { c.Check(worker.Stop(w), gc.IsNil) }() @@ -222,6 +281,110 @@ c.Assert(n1, jc.GreaterThan, n0) } +type publisherFunc func(apiServers [][]instance.HostPort, instanceIds []instance.Id) error + +func (f publisherFunc) publishAPIServers(apiServers [][]instance.HostPort, instanceIds []instance.Id) error { + return f(apiServers, instanceIds) +} + +func (s *workerSuite) TestStateServersArePublished(c *gc.C) { + publishCh := make(chan [][]instance.HostPort) + publish := func(apiServers [][]instance.HostPort, instanceIds []instance.Id) error { + publishCh <- apiServers + return nil + } + + st := newFakeState() + initState(c, st, 3) + w := newWorker(st, publisherFunc(publish)) + defer func() { + c.Check(worker.Stop(w), gc.IsNil) + }() + select { + case servers := <-publishCh: + c.Assert(servers, gc.DeepEquals, expectedAPIHostPorts(3)) + case <-time.After(coretesting.LongWait): + c.Fatalf("timed out waiting for publish") + } + + // Change one of the servers' API addresses and check that it's published. + + newMachine10APIHostPorts := addressesWithPort(apiPort, "0.2.8.124") + st.machine("10").setAPIHostPorts(newMachine10APIHostPorts) + select { + case servers := <-publishCh: + expected := expectedAPIHostPorts(3) + expected[0] = newMachine10APIHostPorts + c.Assert(servers, jc.DeepEquals, expected) + case <-time.After(coretesting.LongWait): + c.Fatalf("timed out waiting for publish") + } +} + +func (s *workerSuite) TestWorkerRetriesOnPublishError(c *gc.C) { + s.PatchValue(&pollInterval, coretesting.LongWait+time.Second) + s.PatchValue(&retryInterval, 5*time.Millisecond) + + publishCh := make(chan [][]instance.HostPort, 100) + + count := 0 + publish := func(apiServers [][]instance.HostPort, instanceIds []instance.Id) error { + publishCh <- apiServers + count++ + if count <= 3 { + return fmt.Errorf("publish error") + } + return nil + } + st := newFakeState() + initState(c, st, 3) + + w := newWorker(st, publisherFunc(publish)) + defer func() { + c.Check(worker.Stop(w), gc.IsNil) + }() + + for i := 0; i < 4; i++ { + select { + case servers := <-publishCh: + c.Assert(servers, jc.DeepEquals, expectedAPIHostPorts(3)) + case <-time.After(coretesting.LongWait): + c.Fatalf("timed out waiting for publish #%d", i) + } + } + select { + case <-publishCh: + c.Errorf("unexpected publish event") + case <-time.After(coretesting.ShortWait): + } +} + +func (s *workerSuite) TestWorkerPublishesInstanceIds(c *gc.C) { + s.PatchValue(&pollInterval, coretesting.LongWait+time.Second) + s.PatchValue(&retryInterval, 5*time.Millisecond) + + publishCh := make(chan []instance.Id, 100) + + publish := func(apiServers [][]instance.HostPort, instanceIds []instance.Id) error { + publishCh <- instanceIds + return nil + } + st := newFakeState() + initState(c, st, 3) + + w := newWorker(st, publisherFunc(publish)) + defer func() { + c.Check(worker.Stop(w), gc.IsNil) + }() + + select { + case instanceIds := <-publishCh: + c.Assert(instanceIds, jc.DeepEquals, []instance.Id{"id-10", "id-11", "id-12"}) + case <-time.After(coretesting.LongWait): + c.Errorf("timed out waiting for publish") + } +} + func mustNext(c *gc.C, w *voyeur.Watcher) (val interface{}, ok bool) { done := make(chan struct{}) go func() { @@ -239,3 +402,9 @@ } panic("unreachable") } + +type noPublisher struct{} + +func (noPublisher) publishAPIServers(apiServers [][]instance.HostPort, instanceIds []instance.Id) error { + return nil +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/provisioner.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/provisioner/provisioner.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/provisioner.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/provisioner/provisioner.go 2014-03-27 15:48:42.000000000 +0000 @@ -14,6 +14,7 @@ "launchpad.net/juju-core/environs/config" "launchpad.net/juju-core/instance" apiprovisioner "launchpad.net/juju-core/state/api/provisioner" + apiwatcher "launchpad.net/juju-core/state/api/watcher" "launchpad.net/juju-core/state/watcher" "launchpad.net/juju-core/worker" ) @@ -28,7 +29,7 @@ type Provisioner interface { worker.Worker Stop() error - getWatcher() (Watcher, error) + getMachineWatcher() (apiwatcher.StringsWatcher, error) } // environProvisioner represents a running provisioning worker for machine nodes @@ -103,7 +104,11 @@ } // Start responding to changes in machines, and to any further updates // to the environment config. - machineWatcher, err := p.getWatcher() + machineWatcher, err := p.getMachineWatcher() + if err != nil { + return nil, err + } + retryWatcher, err := p.st.WatchMachineErrorRetry() if err != nil { return nil, err } @@ -112,6 +117,7 @@ safeMode, p.st, machineWatcher, + retryWatcher, p.broker, auth) return task, nil @@ -183,7 +189,7 @@ } } -func (p *environProvisioner) getWatcher() (Watcher, error) { +func (p *environProvisioner) getMachineWatcher() (apiwatcher.StringsWatcher, error) { return p.st.WatchEnvironMachines() } @@ -250,7 +256,7 @@ return p.machine, nil } -func (p *containerProvisioner) getWatcher() (Watcher, error) { +func (p *containerProvisioner) getMachineWatcher() (apiwatcher.StringsWatcher, error) { machine, err := p.getMachine() if err != nil { return nil, err diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/provisioner_task.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/provisioner/provisioner_task.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/provisioner_task.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/provisioner/provisioner_task.go 2014-03-27 15:48:42.000000000 +0000 @@ -16,6 +16,7 @@ "launchpad.net/juju-core/names" "launchpad.net/juju-core/state/api/params" apiprovisioner "launchpad.net/juju-core/state/api/provisioner" + apiwatcher "launchpad.net/juju-core/state/api/watcher" "launchpad.net/juju-core/state/watcher" coretools "launchpad.net/juju-core/tools" "launchpad.net/juju-core/utils" @@ -35,28 +36,25 @@ SetSafeMode(safeMode bool) } -type Watcher interface { - watcher.Errer - watcher.Stopper - Changes() <-chan []string -} - type MachineGetter interface { Machine(tag string) (*apiprovisioner.Machine, error) + MachinesWithTransientErrors() ([]*apiprovisioner.Machine, []params.StatusResult, error) } func NewProvisionerTask( machineTag string, safeMode bool, machineGetter MachineGetter, - watcher Watcher, + machineWatcher apiwatcher.StringsWatcher, + retryWatcher apiwatcher.NotifyWatcher, broker environs.InstanceBroker, auth environs.AuthenticationProvider, ) ProvisionerTask { task := &provisionerTask{ machineTag: machineTag, machineGetter: machineGetter, - machineWatcher: watcher, + machineWatcher: machineWatcher, + retryWatcher: retryWatcher, broker: broker, auth: auth, safeMode: safeMode, @@ -73,7 +71,8 @@ type provisionerTask struct { machineTag string machineGetter MachineGetter - machineWatcher Watcher + machineWatcher apiwatcher.StringsWatcher + retryWatcher apiwatcher.NotifyWatcher broker environs.InstanceBroker tomb tomb.Tomb auth environs.AuthenticationProvider @@ -132,8 +131,6 @@ if !ok { return watcher.MustErr(task.machineWatcher) } - // TODO(dfc; lp:1042717) fire process machines periodically to shut down unknown - // instances. if err := task.processMachines(ids); err != nil { return fmt.Errorf("failed to process updated machines: %v", err) } @@ -152,6 +149,10 @@ return fmt.Errorf("failed to process machines after safe mode disabled: %v", err) } } + case <-task.retryWatcher.Changes(): + if err := task.processMachinesWithTransientErrors(); err != nil { + return fmt.Errorf("failed to process machines with transient errors: %v", err) + } } } } @@ -164,6 +165,29 @@ } } +func (task *provisionerTask) processMachinesWithTransientErrors() error { + machines, statusResults, err := task.machineGetter.MachinesWithTransientErrors() + if err != nil { + return nil + } + logger.Tracef("processMachinesWithTransientErrors(%v)", statusResults) + var pending []*apiprovisioner.Machine + for i, status := range statusResults { + if status.Error != nil { + logger.Errorf("cannot retry provisioning of machine %q: %v", status.Id, status.Error) + continue + } + machine := machines[i] + if err := machine.SetStatus(params.StatusPending, "", nil); err != nil { + logger.Errorf("cannot reset status of machine %q: %v", status.Id, err) + continue + } + task.machines[machine.Tag()] = machine + pending = append(pending, machine) + } + return task.startMachines(pending) +} + func (task *provisionerTask) processMachines(ids []string) error { logger.Tracef("processMachines(%v)", ids) // Populate the tasks maps of current instances and machines. @@ -405,7 +429,7 @@ // time until the error is resolved, but don't return an // error; just keep going with the other machines. logger.Errorf("cannot start instance for machine %q: %v", machine, err) - if err1 := machine.SetStatus(params.StatusError, err.Error()); err1 != nil { + if err1 := machine.SetStatus(params.StatusError, err.Error(), nil); err1 != nil { // Something is wrong with this machine, better report it back. logger.Errorf("cannot set error status for machine %q: %v", machine, err1) return err1 diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/provisioner_test.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/provisioner/provisioner_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/provisioner/provisioner_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/provisioner/provisioner_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -14,6 +14,8 @@ "launchpad.net/juju-core/constraints" "launchpad.net/juju-core/environs" "launchpad.net/juju-core/environs/config" + "launchpad.net/juju-core/environs/simplestreams" + "launchpad.net/juju-core/environs/tools" "launchpad.net/juju-core/errors" "launchpad.net/juju-core/instance" "launchpad.net/juju-core/juju/testing" @@ -23,6 +25,7 @@ "launchpad.net/juju-core/state/api" "launchpad.net/juju-core/state/api/params" apiprovisioner "launchpad.net/juju-core/state/api/provisioner" + apiserverprovisioner "launchpad.net/juju-core/state/apiserver/provisioner" coretesting "launchpad.net/juju-core/testing" "launchpad.net/juju-core/utils" "launchpad.net/juju-core/utils/set" @@ -731,17 +734,21 @@ s.waitRemoved(c, m3) } -func (s *ProvisionerSuite) newProvisionerTask(c *gc.C, safeMode bool) provisioner.ProvisionerTask { - env := s.APIConn.Environ - watcher, err := s.provisioner.WatchEnvironMachines() +func (s *ProvisionerSuite) newProvisionerTask(c *gc.C, safeMode bool, + broker environs.InstanceBroker) provisioner.ProvisionerTask { + + machineWatcher, err := s.provisioner.WatchEnvironMachines() + c.Assert(err, gc.IsNil) + retryWatcher, err := s.provisioner.WatchMachineErrorRetry() c.Assert(err, gc.IsNil) auth, err := environs.NewAPIAuthenticator(s.provisioner) c.Assert(err, gc.IsNil) - return provisioner.NewProvisionerTask("machine-0", safeMode, s.provisioner, watcher, env, auth) + return provisioner.NewProvisionerTask( + "machine-0", safeMode, s.provisioner, machineWatcher, retryWatcher, broker, auth) } func (s *ProvisionerSuite) TestTurningOffSafeModeReapsUnknownInstances(c *gc.C) { - task := s.newProvisionerTask(c, true) + task := s.newProvisionerTask(c, true, s.APIConn.Environ) defer stop(c, task) // Initially create a machine, and an unknown instance, with safe mode on. @@ -761,3 +768,66 @@ task.SetSafeMode(false) s.checkStopInstances(c, i1) } + +func (s *ProvisionerSuite) TestProvisionerRetriesTransientErrors(c *gc.C) { + s.PatchValue(&apiserverprovisioner.ErrorRetryWaitDelay, 5*time.Millisecond) + var e environs.Environ = &mockBroker{Environ: s.APIConn.Environ, retryCount: make(map[string]int)} + task := s.newProvisionerTask(c, false, e) + defer stop(c, task) + + // Provision some machines, some will be started first time, + // another will require retries. + m1, err := s.addMachine() + c.Assert(err, gc.IsNil) + m2, err := s.addMachine() + c.Assert(err, gc.IsNil) + m3, err := s.addMachine() + c.Assert(err, gc.IsNil) + m4, err := s.addMachine() + c.Assert(err, gc.IsNil) + s.checkStartInstance(c, m1) + s.checkStartInstance(c, m2) + thatsAllFolks := make(chan struct{}) + go func() { + for { + select { + case <-thatsAllFolks: + return + case <-time.After(coretesting.ShortWait): + err := m3.SetStatus(params.StatusError, "info", params.StatusData{"transient": true}) + c.Assert(err, gc.IsNil) + } + } + }() + s.checkStartInstance(c, m3) + close(thatsAllFolks) + // Machine 4 is never provisioned. + status, _, _, err := m4.Status() + c.Assert(err, gc.IsNil) + c.Assert(status, gc.Equals, params.StatusError) + _, err = m4.InstanceId() + c.Assert(err, jc.Satisfies, state.IsNotProvisionedError) +} + +type mockBroker struct { + environs.Environ + retryCount map[string]int +} + +func (b *mockBroker) StartInstance(args environs.StartInstanceParams) (instance.Instance, *instance.HardwareCharacteristics, error) { + // All machines except machines 3, 4 are provisioned successfully the first time. + // Machines 3 is provisioned after some attempts have been made. + // Machine 4 is never provisioned. + id := args.MachineConfig.MachineId + retries := b.retryCount[id] + if (id != "3" && id != "4") || retries > 2 { + return b.Environ.StartInstance(args) + } else { + b.retryCount[id] = retries + 1 + } + return nil, nil, fmt.Errorf("error: some error") +} + +func (b *mockBroker) GetToolsSources() ([]simplestreams.DataSource, error) { + return b.Environ.(tools.SupportsCustomSources).GetToolsSources() +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/bundles.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/uniter/charm/bundles.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/bundles.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/uniter/charm/bundles.go 2014-03-27 15:48:42.000000000 +0000 @@ -0,0 +1,111 @@ +// Copyright 2012-2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package charm + +import ( + "fmt" + "os" + "path" + + "launchpad.net/juju-core/charm" + "launchpad.net/juju-core/downloader" + "launchpad.net/juju-core/utils" +) + +// BundlesDir is responsible for storing and retrieving charm bundles +// identified by state charms. +type BundlesDir struct { + path string +} + +// NewBundlesDir returns a new BundlesDir which uses path for storage. +func NewBundlesDir(path string) *BundlesDir { + return &BundlesDir{path} +} + +// Read returns a charm bundle from the directory. If no bundle exists yet, +// one will be downloaded and validated and copied into the directory before +// being returned. Downloads will be aborted if a value is received on abort. +func (d *BundlesDir) Read(info BundleInfo, abort <-chan struct{}) (Bundle, error) { + path := d.bundlePath(info) + if _, err := os.Stat(path); err != nil { + if !os.IsNotExist(err) { + return nil, err + } else if err = d.download(info, abort); err != nil { + return nil, err + } + } + return charm.ReadBundle(path) +} + +// download fetches the supplied charm and checks that it has the correct sha256 +// hash, then copies it into the directory. If a value is received on abort, the +// download will be stopped. +func (d *BundlesDir) download(info BundleInfo, abort <-chan struct{}) (err error) { + archiveURL, disableSSLHostnameVerification, err := info.ArchiveURL() + if err != nil { + return err + } + defer utils.ErrorContextf(&err, "failed to download charm %q from %q", info.URL(), archiveURL) + dir := d.downloadsPath() + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + aurl := archiveURL.String() + logger.Infof("downloading %s from %s", info.URL(), aurl) + if disableSSLHostnameVerification { + logger.Infof("SSL hostname verification disabled") + } + dl := downloader.New(aurl, dir, disableSSLHostnameVerification) + defer dl.Stop() + for { + select { + case <-abort: + logger.Infof("download aborted") + return fmt.Errorf("aborted") + case st := <-dl.Done(): + if st.Err != nil { + return st.Err + } + logger.Infof("download complete") + defer st.File.Close() + actualSha256, _, err := utils.ReadSHA256(st.File) + if err != nil { + return err + } + archiveSha256, err := info.ArchiveSha256() + if err != nil { + return err + } + if actualSha256 != archiveSha256 { + return fmt.Errorf( + "expected sha256 %q, got %q", archiveSha256, actualSha256, + ) + } + logger.Infof("download verified") + if err := os.MkdirAll(d.path, 0755); err != nil { + return err + } + return os.Rename(st.File.Name(), d.bundlePath(info)) + } + } +} + +// bundlePath returns the path to the location where the verified charm +// bundle identified by info will be, or has been, saved. +func (d *BundlesDir) bundlePath(info BundleInfo) string { + return d.bundleURLPath(info.URL()) +} + +// bundleURLPath returns the path to the location where the verified charm +// bundle identified by url will be, or has been, saved. +func (d *BundlesDir) bundleURLPath(url *charm.URL) string { + return path.Join(d.path, charm.Quote(url.String())) +} + +// downloadsPath returns the path to the directory into which charms are +// downloaded. +func (d *BundlesDir) downloadsPath() string { + return path.Join(d.path, "downloads") +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/bundles_test.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/uniter/charm/bundles_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/bundles_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/uniter/charm/bundles_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -0,0 +1,158 @@ +// Copyright 2012-2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package charm_test + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "io/ioutil" + "net/url" + "os" + "path/filepath" + "time" + + jc "github.com/juju/testing/checkers" + gc "launchpad.net/gocheck" + + corecharm "launchpad.net/juju-core/charm" + "launchpad.net/juju-core/juju/testing" + "launchpad.net/juju-core/state" + "launchpad.net/juju-core/state/api" + "launchpad.net/juju-core/state/api/uniter" + coretesting "launchpad.net/juju-core/testing" + "launchpad.net/juju-core/utils" + "launchpad.net/juju-core/worker/uniter/charm" +) + +type BundlesDirSuite struct { + coretesting.HTTPSuite + testing.JujuConnSuite + + st *api.State + uniter *uniter.State +} + +var _ = gc.Suite(&BundlesDirSuite{}) + +func (s *BundlesDirSuite) SetUpSuite(c *gc.C) { + s.HTTPSuite.SetUpSuite(c) + s.JujuConnSuite.SetUpSuite(c) +} + +func (s *BundlesDirSuite) TearDownSuite(c *gc.C) { + s.JujuConnSuite.TearDownSuite(c) + s.HTTPSuite.TearDownSuite(c) +} + +func (s *BundlesDirSuite) SetUpTest(c *gc.C) { + s.HTTPSuite.SetUpTest(c) + s.JujuConnSuite.SetUpTest(c) + + // Add a charm, service and unit to login to the API with. + charm := s.AddTestingCharm(c, "wordpress") + service := s.AddTestingService(c, "wordpress", charm) + unit, err := service.AddUnit() + c.Assert(err, gc.IsNil) + password, err := utils.RandomPassword() + c.Assert(err, gc.IsNil) + err = unit.SetPassword(password) + c.Assert(err, gc.IsNil) + + s.st = s.OpenAPIAs(c, unit.Tag(), password) + c.Assert(s.st, gc.NotNil) + s.uniter = s.st.Uniter() + c.Assert(s.uniter, gc.NotNil) +} + +func (s *BundlesDirSuite) TearDownTest(c *gc.C) { + err := s.st.Close() + c.Assert(err, gc.IsNil) + s.JujuConnSuite.TearDownTest(c) + s.HTTPSuite.TearDownTest(c) +} + +func (s *BundlesDirSuite) AddCharm(c *gc.C) (*uniter.Charm, *state.Charm, []byte) { + curl := corecharm.MustParseURL("cs:quantal/dummy-1") + surl, err := url.Parse(s.URL("/some/charm.bundle")) + c.Assert(err, gc.IsNil) + bunpath := coretesting.Charms.BundlePath(c.MkDir(), "dummy") + bun, err := corecharm.ReadBundle(bunpath) + c.Assert(err, gc.IsNil) + bundata, hash := readHash(c, bunpath) + sch, err := s.State.AddCharm(bun, curl, surl, hash) + c.Assert(err, gc.IsNil) + apiCharm, err := s.uniter.Charm(sch.URL()) + c.Assert(err, gc.IsNil) + return apiCharm, sch, bundata +} + +func (s *BundlesDirSuite) TestGet(c *gc.C) { + basedir := c.MkDir() + bunsdir := filepath.Join(basedir, "random", "bundles") + d := charm.NewBundlesDir(bunsdir) + + // Check it doesn't get created until it's needed. + _, err := os.Stat(bunsdir) + c.Assert(err, jc.Satisfies, os.IsNotExist) + + // Add a charm to state that we can try to get. + apiCharm, sch, bundata := s.AddCharm(c) + + // Try to get the charm when the content doesn't match. + coretesting.Server.Response(200, nil, []byte("roflcopter")) + _, err = d.Read(apiCharm, nil) + prefix := fmt.Sprintf(`failed to download charm "cs:quantal/dummy-1" from %q: `, sch.BundleURL()) + c.Assert(err, gc.ErrorMatches, prefix+fmt.Sprintf(`expected sha256 %q, got ".*"`, sch.BundleSha256())) + + // Try to get a charm whose bundle doesn't exist. + coretesting.Server.Response(404, nil, nil) + _, err = d.Read(apiCharm, nil) + c.Assert(err, gc.ErrorMatches, prefix+`.* 404 Not Found`) + + // Get a charm whose bundle exists and whose content matches. + coretesting.Server.Response(200, nil, bundata) + ch, err := d.Read(apiCharm, nil) + c.Assert(err, gc.IsNil) + assertCharm(c, ch, sch) + + // Get the same charm again, without preparing a response from the server. + ch, err = d.Read(apiCharm, nil) + c.Assert(err, gc.IsNil) + assertCharm(c, ch, sch) + + // Abort a download. + err = os.RemoveAll(bunsdir) + c.Assert(err, gc.IsNil) + abort := make(chan struct{}) + done := make(chan bool) + go func() { + ch, err := d.Read(apiCharm, abort) + c.Assert(ch, gc.IsNil) + c.Assert(err, gc.ErrorMatches, prefix+"aborted") + close(done) + }() + close(abort) + coretesting.Server.Response(500, nil, nil) + select { + case <-done: + case <-time.After(coretesting.LongWait): + c.Fatalf("timed out waiting for abort") + } +} + +func readHash(c *gc.C, path string) ([]byte, string) { + data, err := ioutil.ReadFile(path) + c.Assert(err, gc.IsNil) + hash := sha256.New() + hash.Write(data) + return data, hex.EncodeToString(hash.Sum(nil)) +} + +func assertCharm(c *gc.C, bun charm.Bundle, sch *state.Charm) { + actual := bun.(*corecharm.Bundle) + c.Assert(actual.Revision(), gc.Equals, sch.Revision()) + c.Assert(actual.Meta(), gc.DeepEquals, sch.Meta()) + c.Assert(actual.Config(), gc.DeepEquals, sch.Config()) +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/charm.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/uniter/charm/charm.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/charm.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/uniter/charm/charm.go 2014-03-27 15:48:42.000000000 +0000 @@ -1,129 +1,100 @@ -// Copyright 2012, 2013 Canonical Ltd. +// Copyright 2012-2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package charm import ( - "fmt" + "errors" "net/url" - "os" - "path" + + "github.com/juju/loggo" "launchpad.net/juju-core/charm" - "launchpad.net/juju-core/downloader" - "launchpad.net/juju-core/log" "launchpad.net/juju-core/utils" + "launchpad.net/juju-core/utils/set" ) -// BundleReader primarily exists to make BundlesDir mockable. -type BundleReader interface { +var logger = loggo.GetLogger("juju.worker.uniter.charm") - // Read returns the bundle identified by the supplied info. The abort chan - // can be used to notify an implementation that it need not complete the - // operation, and can immediately error out if it is convenient to do so. - Read(bi BundleInfo, abort <-chan struct{}) (*charm.Bundle, error) +// charmURLPath is the path within a charm directory to which Deployers +// commonly write the charm URL of the latest deployed charm. +const charmURLPath = ".juju-charm" + +// Bundle allows access to a charm's files. +type Bundle interface { + + // Manifest returns a set of slash-separated strings representing files, + // directories, and symlinks stored in the bundle. + Manifest() (set.Strings, error) + + // ExpandTo unpacks the entities referenced in the manifest into the + // supplied directory. If it returns without error, every file referenced + // in the charm must be present in the directory; implementations may vary + // in the details of what they do with other files present. + ExpandTo(dir string) error } -// BundleInfo holds bundle information for a charm. +// BundleInfo describes a Bundle. type BundleInfo interface { + + // URL returns the charm URL identifying the bundle. URL() *charm.URL - ArchiveURL() (*url.URL, bool, error) + + // Archive URL returns the location of the bundle data. + ArchiveURL() (*url.URL, utils.SSLHostnameVerification, error) + + // ArchiveSha256 returns the hex-encoded SHA-256 digest of the bundle data. ArchiveSha256() (string, error) } -// BundlesDir is responsible for storing and retrieving charm bundles -// identified by state charms. -type BundlesDir struct { - path string -} - -// NewBundlesDir returns a new BundlesDir which uses path for storage. -func NewBundlesDir(path string) *BundlesDir { - return &BundlesDir{path} -} - -// Read returns a charm bundle from the directory. If no bundle exists yet, -// one will be downloaded and validated and copied into the directory before -// being returned. Downloads will be aborted if a value is received on abort. -func (d *BundlesDir) Read(info BundleInfo, abort <-chan struct{}) (*charm.Bundle, error) { - path := d.bundlePath(info) - if _, err := os.Stat(path); err != nil { - if !os.IsNotExist(err) { - return nil, err - } else if err = d.download(info, abort); err != nil { - return nil, err - } - } - return charm.ReadBundle(path) +// BundleReader provides a mechanism for getting a Bundle from a BundleInfo. +type BundleReader interface { + + // Read returns the bundle identified by the supplied info. The abort chan + // can be used to notify an implementation that it need not complete the + // operation, and can immediately error out if it is convenient to do so. + Read(bi BundleInfo, abort <-chan struct{}) (Bundle, error) } -// download fetches the supplied charm and checks that it has the correct sha256 -// hash, then copies it into the directory. If a value is received on abort, the -// download will be stopped. -func (d *BundlesDir) download(info BundleInfo, abort <-chan struct{}) (err error) { - archiveURL, disableSSLHostnameVerification, err := info.ArchiveURL() - if err != nil { - return err - } - defer utils.ErrorContextf(&err, "failed to download charm %q from %q", info.URL(), archiveURL) - dir := d.downloadsPath() - if err := os.MkdirAll(dir, 0755); err != nil { - return err - } - aurl := archiveURL.String() - log.Infof("worker/uniter/charm: downloading %s from %s", info.URL(), aurl) - if disableSSLHostnameVerification { - log.Infof("worker/uniter/charm: SSL hostname verification disabled") - } - dl := downloader.New(aurl, dir, disableSSLHostnameVerification) - defer dl.Stop() - for { - select { - case <-abort: - log.Infof("worker/uniter/charm: download aborted") - return fmt.Errorf("aborted") - case st := <-dl.Done(): - if st.Err != nil { - return st.Err - } - log.Infof("worker/uniter/charm: download complete") - defer st.File.Close() - actualSha256, _, err := utils.ReadSHA256(st.File) - if err != nil { - return err - } - archiveSha256, err := info.ArchiveSha256() - if err != nil { - return err - } - if actualSha256 != archiveSha256 { - return fmt.Errorf( - "expected sha256 %q, got %q", archiveSha256, actualSha256, - ) - } - log.Infof("worker/uniter/charm: download verified") - if err := os.MkdirAll(d.path, 0755); err != nil { - return err - } - return os.Rename(st.File.Name(), d.bundlePath(info)) - } +// Deployer is responsible for installing and upgrading charms. +type Deployer interface { + + // Stage must be called to prime the Deployer to install or upgrade the + // bundle identified by the supplied info. The abort chan can be used to + // notify an implementation that it need not complete the operation, and + // can immediately error out if it convenient to do so. It must always + // be safe to restage the same bundle, or to stage a new bundle. + Stage(info BundleInfo, abort <-chan struct{}) error + + // Deploy will install or upgrade the most recently staged bundle. + // Behaviour is undefined if Stage has not been called. Failures that + // can be resolved by user intervention will be signalled by returning + // ErrConflict. + Deploy() error + + // NotifyRevert must be called when a conflicted deploy is abandoned, in + // preparation for a new upgrade. + NotifyRevert() error + + // NotifyResolved must be called when the cause of a deploy conflict has + // been resolved, and a new deploy attempt will be made. + NotifyResolved() error +} + +// ErrConflict indicates that an upgrade failed and cannot be resolved +// without human intervention. +var ErrConflict = errors.New("charm upgrade has conflicts") + +// ReadCharmURL reads a charm identity file from the supplied path. +func ReadCharmURL(path string) (*charm.URL, error) { + surl := "" + if err := utils.ReadYaml(path, &surl); err != nil { + return nil, err } + return charm.ParseURL(surl) } -// bundlePath returns the path to the location where the verified charm -// bundle identified by info will be, or has been, saved. -func (d *BundlesDir) bundlePath(info BundleInfo) string { - return d.bundleURLPath(info.URL()) -} - -// bundleURLPath returns the path to the location where the verified charm -// bundle identified by url will be, or has been, saved. -func (d *BundlesDir) bundleURLPath(url *charm.URL) string { - return path.Join(d.path, charm.Quote(url.String())) -} - -// downloadsPath returns the path to the directory into which charms are -// downloaded. -func (d *BundlesDir) downloadsPath() string { - return path.Join(d.path, "downloads") +// WriteCharmURL writes a charm identity file into the supplied path. +func WriteCharmURL(path string, url *charm.URL) error { + return utils.WriteYaml(path, url.String()) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/charm_test.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/uniter/charm/charm_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/charm_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/uniter/charm/charm_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -1,162 +1,89 @@ -// Copyright 2012, 2013 Canonical Ltd. +// Copyright 2012-2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package charm_test import ( - "crypto/sha256" - "encoding/hex" "fmt" - "io/ioutil" - "net/url" "os" "path/filepath" stdtesting "testing" - "time" - jc "github.com/juju/testing/checkers" gc "launchpad.net/gocheck" corecharm "launchpad.net/juju-core/charm" - "launchpad.net/juju-core/juju/testing" - "launchpad.net/juju-core/state" - "launchpad.net/juju-core/state/api" - "launchpad.net/juju-core/state/api/uniter" coretesting "launchpad.net/juju-core/testing" - "launchpad.net/juju-core/utils" + "launchpad.net/juju-core/utils/set" "launchpad.net/juju-core/worker/uniter/charm" ) func TestPackage(t *stdtesting.T) { + // TODO(fwereade) 2014-03-21 not-worth-a-bug-number + // rewrite BundlesDir tests to use the mocks below and not require an API + // server and associated gubbins. coretesting.MgoTestPackage(t) } -type BundlesDirSuite struct { - coretesting.HTTPSuite - testing.JujuConnSuite - - st *api.State - uniter *uniter.State -} - -var _ = gc.Suite(&BundlesDirSuite{}) - -func (s *BundlesDirSuite) SetUpSuite(c *gc.C) { - s.HTTPSuite.SetUpSuite(c) - s.JujuConnSuite.SetUpSuite(c) +// bundleReader is a charm.BundleReader that lets us mock out the bundles we +// deploy to test the Deployers. +type bundleReader struct { + bundles map[string]charm.Bundle } -func (s *BundlesDirSuite) TearDownSuite(c *gc.C) { - s.JujuConnSuite.TearDownSuite(c) - s.HTTPSuite.TearDownSuite(c) +// Read implements the BundleReader interface. +func (br *bundleReader) Read(info charm.BundleInfo, abort <-chan struct{}) (charm.Bundle, error) { + bundle, ok := br.bundles[info.URL().String()] + if !ok { + return nil, fmt.Errorf("no such charm!") + } + return bundle, nil } -func (s *BundlesDirSuite) SetUpTest(c *gc.C) { - s.HTTPSuite.SetUpTest(c) - s.JujuConnSuite.SetUpTest(c) - - // Add a charm, service and unit to login to the API with. - charm := s.AddTestingCharm(c, "wordpress") - service := s.AddTestingService(c, "wordpress", charm) - unit, err := service.AddUnit() +func (br *bundleReader) AddCustomBundle(c *gc.C, url *corecharm.URL, customize func(path string)) charm.BundleInfo { + base := c.MkDir() + dirpath := coretesting.Charms.ClonedDirPath(base, "dummy") + customize(dirpath) + dir, err := corecharm.ReadDir(dirpath) c.Assert(err, gc.IsNil) - password, err := utils.RandomPassword() + err = dir.SetDiskRevision(url.Revision) c.Assert(err, gc.IsNil) - err = unit.SetPassword(password) + bunpath := filepath.Join(base, "bundle") + file, err := os.Create(bunpath) c.Assert(err, gc.IsNil) - - s.st = s.OpenAPIAs(c, unit.Tag(), password) - c.Assert(s.st, gc.NotNil) - s.uniter = s.st.Uniter() - c.Assert(s.uniter, gc.NotNil) -} - -func (s *BundlesDirSuite) TearDownTest(c *gc.C) { - err := s.st.Close() + defer file.Close() + err = dir.BundleTo(file) c.Assert(err, gc.IsNil) - s.JujuConnSuite.TearDownTest(c) - s.HTTPSuite.TearDownTest(c) + bundle, err := corecharm.ReadBundle(bunpath) + c.Assert(err, gc.IsNil) + return br.AddBundle(c, url, bundle) } -func (s *BundlesDirSuite) AddCharm(c *gc.C) (*uniter.Charm, *state.Charm, []byte) { - curl := corecharm.MustParseURL("cs:quantal/dummy-1") - surl, err := url.Parse(s.URL("/some/charm.bundle")) - c.Assert(err, gc.IsNil) - bunpath := coretesting.Charms.BundlePath(c.MkDir(), "dummy") - bun, err := corecharm.ReadBundle(bunpath) - c.Assert(err, gc.IsNil) - bundata, hash := readHash(c, bunpath) - sch, err := s.State.AddCharm(bun, curl, surl, hash) - c.Assert(err, gc.IsNil) - apiCharm, err := s.uniter.Charm(sch.URL()) - c.Assert(err, gc.IsNil) - return apiCharm, sch, bundata +func (br *bundleReader) AddBundle(c *gc.C, url *corecharm.URL, bundle charm.Bundle) charm.BundleInfo { + if br.bundles == nil { + br.bundles = map[string]charm.Bundle{} + } + br.bundles[url.String()] = bundle + return &bundleInfo{nil, url} } -func (s *BundlesDirSuite) TestGet(c *gc.C) { - basedir := c.MkDir() - bunsdir := filepath.Join(basedir, "random", "bundles") - d := charm.NewBundlesDir(bunsdir) - - // Check it doesn't get created until it's needed. - _, err := os.Stat(bunsdir) - c.Assert(err, jc.Satisfies, os.IsNotExist) - - // Add a charm to state that we can try to get. - apiCharm, sch, bundata := s.AddCharm(c) - - // Try to get the charm when the content doesn't match. - coretesting.Server.Response(200, nil, []byte("roflcopter")) - _, err = d.Read(apiCharm, nil) - prefix := fmt.Sprintf(`failed to download charm "cs:quantal/dummy-1" from %q: `, sch.BundleURL()) - c.Assert(err, gc.ErrorMatches, prefix+fmt.Sprintf(`expected sha256 %q, got ".*"`, sch.BundleSha256())) - - // Try to get a charm whose bundle doesn't exist. - coretesting.Server.Response(404, nil, nil) - _, err = d.Read(apiCharm, nil) - c.Assert(err, gc.ErrorMatches, prefix+`.* 404 Not Found`) - - // Get a charm whose bundle exists and whose content matches. - coretesting.Server.Response(200, nil, bundata) - ch, err := d.Read(apiCharm, nil) - c.Assert(err, gc.IsNil) - assertCharm(c, ch, sch) +type bundleInfo struct { + charm.BundleInfo + url *corecharm.URL +} - // Get the same charm again, without preparing a response from the server. - ch, err = d.Read(apiCharm, nil) - c.Assert(err, gc.IsNil) - assertCharm(c, ch, sch) +func (info *bundleInfo) URL() *corecharm.URL { + return info.url +} - // Abort a download. - err = os.RemoveAll(bunsdir) - c.Assert(err, gc.IsNil) - abort := make(chan struct{}) - done := make(chan bool) - go func() { - ch, err := d.Read(apiCharm, abort) - c.Assert(ch, gc.IsNil) - c.Assert(err, gc.ErrorMatches, prefix+"aborted") - close(done) - }() - close(abort) - coretesting.Server.Response(500, nil, nil) - select { - case <-done: - case <-time.After(coretesting.LongWait): - c.Fatalf("timed out waiting for abort") - } +type mockBundle struct { + paths set.Strings + expand func(dir string) error } -func readHash(c *gc.C, path string) ([]byte, string) { - data, err := ioutil.ReadFile(path) - c.Assert(err, gc.IsNil) - hash := sha256.New() - hash.Write(data) - return data, hex.EncodeToString(hash.Sum(nil)) +func (b mockBundle) Manifest() (set.Strings, error) { + return set.NewStrings(b.paths.Values()...), nil } -func assertCharm(c *gc.C, bun *corecharm.Bundle, sch *state.Charm) { - c.Assert(bun.Revision(), gc.Equals, sch.Revision()) - c.Assert(bun.Meta(), gc.DeepEquals, sch.Meta()) - c.Assert(bun.Config(), gc.DeepEquals, sch.Config()) +func (b mockBundle) ExpandTo(dir string) error { + return b.expand(dir) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/deployer.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/uniter/charm/deployer.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/deployer.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/uniter/charm/deployer.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,249 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package charm - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "time" - - "launchpad.net/juju-core/log" -) - -const ( - updatePrefix = "update-" - installPrefix = "install-" -) - -// Deployer is responsible for installing and upgrading charms. -type Deployer interface { - - // Stage must be called to prime the Deployer to install or upgrade the - // bundle identified by the supplied info. The abort chan can be used to - // notify an implementation that it need not complete the operation, and - // can immediately error out if it convenient to do so. It must always - // be safe to restage the same bundle, or to stage a new bundle. - Stage(info BundleInfo, abort <-chan struct{}) error - - // Deploy will install or upgrade the most recently staged bundle. - // Behaviour is undefined if Stage has not been called. - Deploy() error - - // NotifyRevert must be called when a conflicted deploy is abandoned, in - // preparation for a new upgrade. - NotifyRevert() error - - // NotifyResolved must be called when the cause of a deploy conflict has - // been resolved, and a new deploy attempt will be made. - NotifyResolved() error -} - -// gitDeployer maintains a git repository tracking a series of charm versions, -// and can install and upgrade charm deployments to the current version. -type gitDeployer struct { - target *GitDir - dataPath string - bundles BundleReader - current *GitDir -} - -// NewGitDeployer creates a new Deployer which stores its state in dataPath, -// and installs or upgrades the charm at charmPath. -func NewGitDeployer(charmPath, dataPath string, bundles BundleReader) Deployer { - return &gitDeployer{ - target: NewGitDir(charmPath), - dataPath: dataPath, - bundles: bundles, - current: NewGitDir(filepath.Join(dataPath, "current")), - } -} - -func (d *gitDeployer) Stage(info BundleInfo, abort <-chan struct{}) error { - // Make sure we've got an actual bundle available. - bundle, err := d.bundles.Read(info, abort) - if err != nil { - return err - } - - // Read present state of current. - if err := os.MkdirAll(d.dataPath, 0755); err != nil { - return err - } - defer d.collectOrphans() - srcExists, err := d.current.Exists() - if err != nil { - return err - } - url := info.URL() - if srcExists { - prevURL, err := ReadCharmURL(d.current) - if err != nil { - return err - } - if *url == *prevURL { - return nil - } - } - - // Prepare a fresh repository for the update, using current's history - // if it exists. - updatePath, err := d.newDir(updatePrefix) - if err != nil { - return err - } - var repo *GitDir - if srcExists { - repo, err = d.current.Clone(updatePath) - } else { - repo = NewGitDir(updatePath) - err = repo.Init() - } - if err != nil { - return err - } - - // Write the desired new state and commit. - if err = bundle.ExpandTo(updatePath); err != nil { - return err - } - if err = WriteCharmURL(repo, url); err != nil { - return err - } - if err = repo.Snapshotf("Imported charm %q from %q.", url, bundle.Path); err != nil { - return err - } - - // Atomically rename fresh repository to current. - tmplink := filepath.Join(updatePath, "tmplink") - if err = os.Symlink(updatePath, tmplink); err != nil { - return err - } - return os.Rename(tmplink, d.current.Path()) -} - -func (d *gitDeployer) Deploy() (err error) { - defer func() { - if err == ErrConflict { - log.Warningf("worker/uniter/charm: charm deployment completed with conflicts") - } else if err != nil { - err = fmt.Errorf("charm deployment failed: %s", err) - log.Errorf("worker/uniter/charm: %v", err) - } else { - log.Infof("worker/uniter/charm: charm deployment succeeded") - } - }() - if exists, err := d.current.Exists(); err != nil { - return err - } else if !exists { - return fmt.Errorf("no charm set") - } - if exists, err := d.target.Exists(); err != nil { - return err - } else if !exists { - return d.install() - } - return d.upgrade() -} - -func (d *gitDeployer) NotifyRevert() error { - return d.target.Revert() -} - -func (d *gitDeployer) NotifyResolved() error { - return d.target.Snapshotf("Upgrade conflict resolved.") -} - -// install creates a new deployment of current, and atomically moves it to -// target. -func (d *gitDeployer) install() error { - defer d.collectOrphans() - log.Infof("worker/uniter/charm: preparing new charm deployment") - url, err := ReadCharmURL(d.current) - if err != nil { - return err - } - installPath, err := d.newDir(installPrefix) - if err != nil { - return err - } - repo := NewGitDir(installPath) - if err = repo.Init(); err != nil { - return err - } - if err = repo.Pull(d.current); err != nil { - return err - } - if err = repo.Snapshotf("Deployed charm %q.", url); err != nil { - return err - } - log.Infof("worker/uniter/charm: deploying charm") - return os.Rename(installPath, d.target.Path()) -} - -// upgrade pulls from current into target. If target has local changes, but -// no conflicts, it will be snapshotted before any changes are made. -func (d *gitDeployer) upgrade() error { - log.Infof("worker/uniter/charm: preparing charm upgrade") - url, err := ReadCharmURL(d.current) - if err != nil { - return err - } - if err := d.target.Init(); err != nil { - return err - } - if dirty, err := d.target.Dirty(); err != nil { - return err - } else if dirty { - if conflicted, err := d.target.Conflicted(); err != nil { - return err - } else if !conflicted { - log.Infof("worker/uniter/charm: snapshotting dirty charm before upgrade") - if err = d.target.Snapshotf("Pre-upgrade snapshot."); err != nil { - return err - } - } - } - log.Infof("worker/uniter/charm: deploying charm") - if err := d.target.Pull(d.current); err != nil { - return err - } - return d.target.Snapshotf("Upgraded charm to %q.", url) -} - -// collectOrphans deletes all repos in dataPath except the one pointed to by current. -// Errors are generally ignored; some are logged. -func (d *gitDeployer) collectOrphans() { - current, err := os.Readlink(d.current.Path()) - if err != nil { - return - } - if !filepath.IsAbs(current) { - current = filepath.Join(d.dataPath, current) - } - orphans, err := filepath.Glob(filepath.Join(d.dataPath, fmt.Sprintf("%s*", updatePrefix))) - if err != nil { - return - } - installOrphans, err := filepath.Glob(filepath.Join(d.dataPath, fmt.Sprintf("%s*", installPrefix))) - if err != nil { - return - } - orphans = append(orphans, installOrphans...) - for _, repoPath := range orphans { - if repoPath != d.dataPath && repoPath != current { - if err = os.RemoveAll(repoPath); err != nil { - log.Warningf("worker/uniter/charm: failed to remove orphan repo at %s: %s", repoPath, err) - } - } - } -} - -// newDir creates a new timestamped directory with the given prefix. It -// assumes that the deployer will not need to create more than 10 -// directories in any given second. -func (d *gitDeployer) newDir(prefix string) (string, error) { - return ioutil.TempDir(d.dataPath, prefix+time.Now().Format("20060102-150405")) -} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/deployer_test.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/uniter/charm/deployer_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/deployer_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/uniter/charm/deployer_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,267 +0,0 @@ -// Copyright 2012, 2013 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package charm_test - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - - gc "launchpad.net/gocheck" - - corecharm "launchpad.net/juju-core/charm" - "launchpad.net/juju-core/testing" - "launchpad.net/juju-core/worker/uniter/charm" -) - -type DeployerSuite struct { - testing.GitSuite - bundles *bundleReader - targetPath string - deployer charm.Deployer -} - -var _ = gc.Suite(&DeployerSuite{}) - -func (s *DeployerSuite) SetUpTest(c *gc.C) { - s.GitSuite.SetUpTest(c) - s.bundles = &bundleReader{} - s.targetPath = filepath.Join(c.MkDir(), "target") - deployerPath := filepath.Join(c.MkDir(), "deployer") - s.deployer = charm.NewGitDeployer(s.targetPath, deployerPath, s.bundles) -} - -func (s *DeployerSuite) TestUnsetCharm(c *gc.C) { - err := s.deployer.Deploy() - c.Assert(err, gc.ErrorMatches, "charm deployment failed: no charm set") -} - -func (s *DeployerSuite) TestInstall(c *gc.C) { - // Prepare. - info := s.bundles.Add(c, corecharm.MustParseURL("cs:s/c-1"), func(path string) { - err := ioutil.WriteFile(filepath.Join(path, "some-file"), []byte("hello"), 0644) - c.Assert(err, gc.IsNil) - }) - err := s.deployer.Stage(info, nil) - c.Assert(err, gc.IsNil) - checkCleanup(c, s.deployer) - - // Install. - err = s.deployer.Deploy() - c.Assert(err, gc.IsNil) - checkCleanup(c, s.deployer) - - // Check content. - data, err := ioutil.ReadFile(filepath.Join(s.targetPath, "some-file")) - c.Assert(err, gc.IsNil) - c.Assert(string(data), gc.Equals, "hello") - - target := charm.NewGitDir(s.targetPath) - url, err := charm.ReadCharmURL(target) - c.Assert(err, gc.IsNil) - c.Assert(url, gc.DeepEquals, corecharm.MustParseURL("cs:s/c-1")) - lines, err := target.Log() - c.Assert(err, gc.IsNil) - c.Assert(lines, gc.HasLen, 2) - c.Assert(lines[0], gc.Matches, `[0-9a-f]{7} Deployed charm "cs:s/c-1".`) - c.Assert(lines[1], gc.Matches, `[0-9a-f]{7} Imported charm "cs:s/c-1" from ".*".`) -} - -func (s *DeployerSuite) TestUpgrade(c *gc.C) { - // Install. - info1 := s.bundles.Add(c, corecharm.MustParseURL("cs:s/c-1"), func(path string) { - err := ioutil.WriteFile(filepath.Join(path, "some-file"), []byte("hello"), 0644) - c.Assert(err, gc.IsNil) - err = os.Symlink("./some-file", filepath.Join(path, "a-symlink")) - c.Assert(err, gc.IsNil) - }) - err := s.deployer.Stage(info1, nil) - c.Assert(err, gc.IsNil) - err = s.deployer.Deploy() - c.Assert(err, gc.IsNil) - - // Upgrade. - info2 := s.bundles.Add(c, corecharm.MustParseURL("cs:s/c-2"), func(path string) { - err := ioutil.WriteFile(filepath.Join(path, "some-file"), []byte("goodbye"), 0644) - c.Assert(err, gc.IsNil) - err = ioutil.WriteFile(filepath.Join(path, "a-symlink"), []byte("not any more!"), 0644) - c.Assert(err, gc.IsNil) - }) - err = s.deployer.Stage(info2, nil) - c.Assert(err, gc.IsNil) - checkCleanup(c, s.deployer) - err = s.deployer.Deploy() - c.Assert(err, gc.IsNil) - checkCleanup(c, s.deployer) - - // Check content. - data, err := ioutil.ReadFile(filepath.Join(s.targetPath, "some-file")) - c.Assert(err, gc.IsNil) - c.Assert(string(data), gc.Equals, "goodbye") - data, err = ioutil.ReadFile(filepath.Join(s.targetPath, "a-symlink")) - c.Assert(err, gc.IsNil) - c.Assert(string(data), gc.Equals, "not any more!") - - target := charm.NewGitDir(s.targetPath) - url, err := charm.ReadCharmURL(target) - c.Assert(err, gc.IsNil) - c.Assert(url, gc.DeepEquals, corecharm.MustParseURL("cs:s/c-2")) - lines, err := target.Log() - c.Assert(err, gc.IsNil) - c.Assert(lines, gc.HasLen, 5) - c.Assert(lines[0], gc.Matches, `[0-9a-f]{7} Upgraded charm to "cs:s/c-2".`) -} - -func (s *DeployerSuite) TestConflictRevertResolve(c *gc.C) { - // Install. - info1 := s.bundles.Add(c, corecharm.MustParseURL("cs:s/c-1"), func(path string) { - err := ioutil.WriteFile(filepath.Join(path, "some-file"), []byte("hello"), 0644) - c.Assert(err, gc.IsNil) - }) - err := s.deployer.Stage(info1, nil) - c.Assert(err, gc.IsNil) - err = s.deployer.Deploy() - c.Assert(err, gc.IsNil) - - // Mess up target. - err = ioutil.WriteFile(filepath.Join(s.targetPath, "some-file"), []byte("mu!"), 0644) - c.Assert(err, gc.IsNil) - - // Upgrade. - info2 := s.bundles.Add(c, corecharm.MustParseURL("cs:s/c-2"), func(path string) { - err := ioutil.WriteFile(filepath.Join(path, "some-file"), []byte("goodbye"), 0644) - c.Assert(err, gc.IsNil) - }) - err = s.deployer.Stage(info2, nil) - c.Assert(err, gc.IsNil) - err = s.deployer.Deploy() - c.Assert(err, gc.Equals, charm.ErrConflict) - checkCleanup(c, s.deployer) - - // Check state. - target := charm.NewGitDir(s.targetPath) - conflicted, err := target.Conflicted() - c.Assert(err, gc.IsNil) - c.Assert(conflicted, gc.Equals, true) - - // Revert and check initial content. - err = s.deployer.NotifyRevert() - c.Assert(err, gc.IsNil) - data, err := ioutil.ReadFile(filepath.Join(s.targetPath, "some-file")) - c.Assert(err, gc.IsNil) - c.Assert(string(data), gc.Equals, "mu!") - conflicted, err = target.Conflicted() - c.Assert(err, gc.IsNil) - c.Assert(conflicted, gc.Equals, false) - - // Try to upgrade again. - err = s.deployer.Deploy() - c.Assert(err, gc.Equals, charm.ErrConflict) - conflicted, err = target.Conflicted() - c.Assert(err, gc.IsNil) - c.Assert(conflicted, gc.Equals, true) - checkCleanup(c, s.deployer) - - // And again. - err = s.deployer.Deploy() - c.Assert(err, gc.Equals, charm.ErrConflict) - conflicted, err = target.Conflicted() - c.Assert(err, gc.IsNil) - c.Assert(conflicted, gc.Equals, true) - checkCleanup(c, s.deployer) - - // Manually resolve, and commit. - err = ioutil.WriteFile(filepath.Join(target.Path(), "some-file"), []byte("nu!"), 0644) - c.Assert(err, gc.IsNil) - err = s.deployer.NotifyResolved() - c.Assert(err, gc.IsNil) - conflicted, err = target.Conflicted() - c.Assert(err, gc.IsNil) - c.Assert(conflicted, gc.Equals, false) - - // Try a final upgrade to the same charm and check it doesn't write anything - // except the upgrade log line. - err = s.deployer.Deploy() - c.Assert(err, gc.IsNil) - checkCleanup(c, s.deployer) - - data, err = ioutil.ReadFile(filepath.Join(target.Path(), "some-file")) - c.Assert(err, gc.IsNil) - c.Assert(string(data), gc.Equals, "nu!") - conflicted, err = target.Conflicted() - c.Assert(err, gc.IsNil) - c.Assert(conflicted, gc.Equals, false) - lines, err := target.Log() - c.Assert(err, gc.IsNil) - c.Assert(lines[0], gc.Matches, `[0-9a-f]{7} Upgraded charm to "cs:s/c-2".`) -} - -func bundle(c *gc.C, customize func(path string)) *corecharm.Bundle { - base := c.MkDir() - dirpath := testing.Charms.ClonedDirPath(base, "dummy") - customize(dirpath) - dir, err := corecharm.ReadDir(dirpath) - c.Assert(err, gc.IsNil) - bunpath := filepath.Join(base, "bundle") - file, err := os.Create(bunpath) - c.Assert(err, gc.IsNil) - defer file.Close() - err = dir.BundleTo(file) - c.Assert(err, gc.IsNil) - bundle, err := corecharm.ReadBundle(bunpath) - c.Assert(err, gc.IsNil) - return bundle -} - -func checkCleanup(c *gc.C, d charm.Deployer) { - // Only one update dir should exist and be pointed to by the 'current' - // symlink since extra ones should have been cleaned up by - // cleanupOrphans. - deployerPath := charm.GitDeployerDataPath(d) - updateDirs, err := filepath.Glob(filepath.Join(deployerPath, "update-*")) - c.Assert(err, gc.IsNil) - c.Assert(updateDirs, gc.HasLen, 1) - deployerCurrent := charm.GitDeployerCurrent(d) - current, err := os.Readlink(deployerCurrent.Path()) - c.Assert(err, gc.IsNil) - c.Assert(updateDirs[0], gc.Equals, current) - - // No install dirs should be left behind since the one created is - // renamed to the target path. - installDirs, err := filepath.Glob(filepath.Join(deployerPath, "install-*")) - c.Assert(err, gc.IsNil) - c.Assert(installDirs, gc.HasLen, 0) -} - -type bundleReader struct { - bundles map[string]*corecharm.Bundle -} - -// Read implements the BundleReader interface. -func (br *bundleReader) Read(info charm.BundleInfo, abort <-chan struct{}) (*corecharm.Bundle, error) { - bundle, ok := br.bundles[info.URL().String()] - if !ok { - return nil, fmt.Errorf("no such charm!") - } - return bundle, nil -} - -func (br *bundleReader) Add(c *gc.C, url *corecharm.URL, customize func(path string)) charm.BundleInfo { - bundle := bundle(c, customize) - if br.bundles == nil { - br.bundles = map[string]*corecharm.Bundle{} - } - br.bundles[url.String()] = bundle - return &bundleInfo{nil, url} -} - -type bundleInfo struct { - charm.BundleInfo - url *corecharm.URL -} - -func (info *bundleInfo) URL() *corecharm.URL { - return info.url -} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/export_test.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/uniter/charm/export_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/export_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/uniter/charm/export_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -1,4 +1,4 @@ -// Copyright 2012, 2013 Canonical Ltd. +// Copyright 2012-2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package charm diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/git_deployer.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/uniter/charm/git_deployer.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/git_deployer.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/uniter/charm/git_deployer.go 2014-03-27 15:48:42.000000000 +0000 @@ -0,0 +1,230 @@ +// Copyright 2012-2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package charm + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "time" +) + +const ( + gitUpdatePrefix = "update-" + gitInstallPrefix = "install-" + gitCurrentPath = "current" +) + +// gitDeployer maintains a git repository tracking a series of charm versions, +// and can install and upgrade charm deployments to the current version. +type gitDeployer struct { + target *GitDir + dataPath string + bundles BundleReader + current *GitDir +} + +// NewGitDeployer creates a new Deployer which stores its state in dataPath, +// and installs or upgrades the charm at charmPath. +func NewGitDeployer(charmPath, dataPath string, bundles BundleReader) Deployer { + return &gitDeployer{ + target: NewGitDir(charmPath), + dataPath: dataPath, + bundles: bundles, + current: NewGitDir(filepath.Join(dataPath, gitCurrentPath)), + } +} + +func (d *gitDeployer) Stage(info BundleInfo, abort <-chan struct{}) error { + // Make sure we've got an actual bundle available. + bundle, err := d.bundles.Read(info, abort) + if err != nil { + return err + } + + // Read present state of current. + if err := os.MkdirAll(d.dataPath, 0755); err != nil { + return err + } + defer collectGitOrphans(d.dataPath) + srcExists, err := d.current.Exists() + if err != nil { + return err + } + url := info.URL() + if srcExists { + prevURL, err := d.current.ReadCharmURL() + if err != nil { + return err + } + if *url == *prevURL { + return nil + } + } + + // Prepare a fresh repository for the update, using current's history + // if it exists. + updatePath, err := d.newDir(gitUpdatePrefix) + if err != nil { + return err + } + var repo *GitDir + if srcExists { + repo, err = d.current.Clone(updatePath) + } else { + repo = NewGitDir(updatePath) + err = repo.Init() + } + if err != nil { + return err + } + + // Write the desired new state and commit. + if err = bundle.ExpandTo(updatePath); err != nil { + return err + } + if err = repo.WriteCharmURL(url); err != nil { + return err + } + if err = repo.Snapshotf("Imported charm %q.", url); err != nil { + return err + } + + // Atomically rename fresh repository to current. + tmplink := filepath.Join(updatePath, "tmplink") + if err = os.Symlink(updatePath, tmplink); err != nil { + return err + } + return os.Rename(tmplink, d.current.Path()) +} + +func (d *gitDeployer) Deploy() (err error) { + defer func() { + if err == ErrConflict { + logger.Warningf("charm deployment completed with conflicts") + } else if err != nil { + err = fmt.Errorf("charm deployment failed: %s", err) + logger.Errorf("%v", err) + } else { + logger.Infof("charm deployment succeeded") + } + }() + if exists, err := d.current.Exists(); err != nil { + return err + } else if !exists { + return fmt.Errorf("no charm set") + } + if exists, err := d.target.Exists(); err != nil { + return err + } else if !exists { + return d.install() + } + return d.upgrade() +} + +func (d *gitDeployer) NotifyRevert() error { + return d.target.Revert() +} + +func (d *gitDeployer) NotifyResolved() error { + return d.target.Snapshotf("Upgrade conflict resolved.") +} + +// install creates a new deployment of current, and atomically moves it to +// target. +func (d *gitDeployer) install() error { + defer collectGitOrphans(d.dataPath) + logger.Infof("preparing new charm deployment") + url, err := d.current.ReadCharmURL() + if err != nil { + return err + } + installPath, err := d.newDir(gitInstallPrefix) + if err != nil { + return err + } + repo := NewGitDir(installPath) + if err = repo.Init(); err != nil { + return err + } + if err = repo.Pull(d.current); err != nil { + return err + } + if err = repo.Snapshotf("Deployed charm %q.", url); err != nil { + return err + } + logger.Infof("deploying charm") + return os.Rename(installPath, d.target.Path()) +} + +// upgrade pulls from current into target. If target has local changes, but +// no conflicts, it will be snapshotted before any changes are made. +func (d *gitDeployer) upgrade() error { + logger.Infof("preparing charm upgrade") + url, err := d.current.ReadCharmURL() + if err != nil { + return err + } + if err := d.target.Init(); err != nil { + return err + } + if dirty, err := d.target.Dirty(); err != nil { + return err + } else if dirty { + if conflicted, err := d.target.Conflicted(); err != nil { + return err + } else if !conflicted { + logger.Infof("snapshotting dirty charm before upgrade") + if err = d.target.Snapshotf("Pre-upgrade snapshot."); err != nil { + return err + } + } + } + logger.Infof("deploying charm") + if err := d.target.Pull(d.current); err != nil { + return err + } + return d.target.Snapshotf("Upgraded charm to %q.", url) +} + +// collectGitOrphans deletes all repos in dataPath except the one pointed to by +// a git deployer's "current" symlink. +// Errors are generally ignored; some are logged. If current does not exist, *all* +// repos are orphans, and all will be deleted; this should only be the case when +// converting a gitDeployer to a manifestDeployer. +func collectGitOrphans(dataPath string) { + current, err := os.Readlink(filepath.Join(dataPath, gitCurrentPath)) + if os.IsNotExist(err) { + logger.Warningf("no current staging repo") + } else if err != nil { + logger.Warningf("cannot read current staging repo: %v", err) + return + } else if !filepath.IsAbs(current) { + current = filepath.Join(dataPath, current) + } + orphans, err := filepath.Glob(filepath.Join(dataPath, fmt.Sprintf("%s*", gitUpdatePrefix))) + if err != nil { + return + } + installOrphans, err := filepath.Glob(filepath.Join(dataPath, fmt.Sprintf("%s*", gitInstallPrefix))) + if err != nil { + return + } + orphans = append(orphans, installOrphans...) + for _, repoPath := range orphans { + if repoPath != dataPath && repoPath != current { + if err = os.RemoveAll(repoPath); err != nil { + logger.Warningf("failed to remove orphan repo at %s: %s", repoPath, err) + } + } + } +} + +// newDir creates a new timestamped directory with the given prefix. It +// assumes that the deployer will not need to create more than 10 +// directories in any given second. +func (d *gitDeployer) newDir(prefix string) (string, error) { + return ioutil.TempDir(d.dataPath, prefix+time.Now().Format("20060102-150405")) +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/git_deployer_test.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/uniter/charm/git_deployer_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/git_deployer_test.go 1970-01-01 00:00:00.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/uniter/charm/git_deployer_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -0,0 +1,218 @@ +// Copyright 2012-2014 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package charm_test + +import ( + "io/ioutil" + "os" + "path/filepath" + + gc "launchpad.net/gocheck" + + corecharm "launchpad.net/juju-core/charm" + "launchpad.net/juju-core/testing" + "launchpad.net/juju-core/worker/uniter/charm" +) + +type GitDeployerSuite struct { + testing.GitSuite + bundles *bundleReader + targetPath string + deployer charm.Deployer +} + +var _ = gc.Suite(&GitDeployerSuite{}) + +func (s *GitDeployerSuite) SetUpTest(c *gc.C) { + s.GitSuite.SetUpTest(c) + s.bundles = &bundleReader{} + s.targetPath = filepath.Join(c.MkDir(), "target") + deployerPath := filepath.Join(c.MkDir(), "deployer") + s.deployer = charm.NewGitDeployer(s.targetPath, deployerPath, s.bundles) +} + +func (s *GitDeployerSuite) TestUnsetCharm(c *gc.C) { + err := s.deployer.Deploy() + c.Assert(err, gc.ErrorMatches, "charm deployment failed: no charm set") +} + +func (s *GitDeployerSuite) TestInstall(c *gc.C) { + // Prepare. + info := s.bundles.AddCustomBundle(c, corecharm.MustParseURL("cs:s/c-1"), func(path string) { + err := ioutil.WriteFile(filepath.Join(path, "some-file"), []byte("hello"), 0644) + c.Assert(err, gc.IsNil) + }) + err := s.deployer.Stage(info, nil) + c.Assert(err, gc.IsNil) + checkCleanup(c, s.deployer) + + // Install. + err = s.deployer.Deploy() + c.Assert(err, gc.IsNil) + checkCleanup(c, s.deployer) + + // Check content. + data, err := ioutil.ReadFile(filepath.Join(s.targetPath, "some-file")) + c.Assert(err, gc.IsNil) + c.Assert(string(data), gc.Equals, "hello") + + target := charm.NewGitDir(s.targetPath) + url, err := target.ReadCharmURL() + c.Assert(err, gc.IsNil) + c.Assert(url, gc.DeepEquals, corecharm.MustParseURL("cs:s/c-1")) + lines, err := target.Log() + c.Assert(err, gc.IsNil) + c.Assert(lines, gc.HasLen, 2) + c.Assert(lines[0], gc.Matches, `[0-9a-f]{7} Deployed charm "cs:s/c-1"\.`) + c.Assert(lines[1], gc.Matches, `[0-9a-f]{7} Imported charm "cs:s/c-1"\.`) +} + +func (s *GitDeployerSuite) TestUpgrade(c *gc.C) { + // Install. + info1 := s.bundles.AddCustomBundle(c, corecharm.MustParseURL("cs:s/c-1"), func(path string) { + err := ioutil.WriteFile(filepath.Join(path, "some-file"), []byte("hello"), 0644) + c.Assert(err, gc.IsNil) + err = os.Symlink("./some-file", filepath.Join(path, "a-symlink")) + c.Assert(err, gc.IsNil) + }) + err := s.deployer.Stage(info1, nil) + c.Assert(err, gc.IsNil) + err = s.deployer.Deploy() + c.Assert(err, gc.IsNil) + + // Upgrade. + info2 := s.bundles.AddCustomBundle(c, corecharm.MustParseURL("cs:s/c-2"), func(path string) { + err := ioutil.WriteFile(filepath.Join(path, "some-file"), []byte("goodbye"), 0644) + c.Assert(err, gc.IsNil) + err = ioutil.WriteFile(filepath.Join(path, "a-symlink"), []byte("not any more!"), 0644) + c.Assert(err, gc.IsNil) + }) + err = s.deployer.Stage(info2, nil) + c.Assert(err, gc.IsNil) + checkCleanup(c, s.deployer) + err = s.deployer.Deploy() + c.Assert(err, gc.IsNil) + checkCleanup(c, s.deployer) + + // Check content. + data, err := ioutil.ReadFile(filepath.Join(s.targetPath, "some-file")) + c.Assert(err, gc.IsNil) + c.Assert(string(data), gc.Equals, "goodbye") + data, err = ioutil.ReadFile(filepath.Join(s.targetPath, "a-symlink")) + c.Assert(err, gc.IsNil) + c.Assert(string(data), gc.Equals, "not any more!") + + target := charm.NewGitDir(s.targetPath) + url, err := target.ReadCharmURL() + c.Assert(err, gc.IsNil) + c.Assert(url, gc.DeepEquals, corecharm.MustParseURL("cs:s/c-2")) + lines, err := target.Log() + c.Assert(err, gc.IsNil) + c.Assert(lines, gc.HasLen, 5) + c.Assert(lines[0], gc.Matches, `[0-9a-f]{7} Upgraded charm to "cs:s/c-2".`) +} + +func (s *GitDeployerSuite) TestConflictRevertResolve(c *gc.C) { + // Install. + info1 := s.bundles.AddCustomBundle(c, corecharm.MustParseURL("cs:s/c-1"), func(path string) { + err := ioutil.WriteFile(filepath.Join(path, "some-file"), []byte("hello"), 0644) + c.Assert(err, gc.IsNil) + }) + err := s.deployer.Stage(info1, nil) + c.Assert(err, gc.IsNil) + err = s.deployer.Deploy() + c.Assert(err, gc.IsNil) + + // Mess up target. + err = ioutil.WriteFile(filepath.Join(s.targetPath, "some-file"), []byte("mu!"), 0644) + c.Assert(err, gc.IsNil) + + // Upgrade. + info2 := s.bundles.AddCustomBundle(c, corecharm.MustParseURL("cs:s/c-2"), func(path string) { + err := ioutil.WriteFile(filepath.Join(path, "some-file"), []byte("goodbye"), 0644) + c.Assert(err, gc.IsNil) + }) + err = s.deployer.Stage(info2, nil) + c.Assert(err, gc.IsNil) + err = s.deployer.Deploy() + c.Assert(err, gc.Equals, charm.ErrConflict) + checkCleanup(c, s.deployer) + + // Check state. + target := charm.NewGitDir(s.targetPath) + conflicted, err := target.Conflicted() + c.Assert(err, gc.IsNil) + c.Assert(conflicted, gc.Equals, true) + + // Revert and check initial content. + err = s.deployer.NotifyRevert() + c.Assert(err, gc.IsNil) + data, err := ioutil.ReadFile(filepath.Join(s.targetPath, "some-file")) + c.Assert(err, gc.IsNil) + c.Assert(string(data), gc.Equals, "mu!") + conflicted, err = target.Conflicted() + c.Assert(err, gc.IsNil) + c.Assert(conflicted, gc.Equals, false) + + // Try to upgrade again. + err = s.deployer.Deploy() + c.Assert(err, gc.Equals, charm.ErrConflict) + conflicted, err = target.Conflicted() + c.Assert(err, gc.IsNil) + c.Assert(conflicted, gc.Equals, true) + checkCleanup(c, s.deployer) + + // And again. + err = s.deployer.Deploy() + c.Assert(err, gc.Equals, charm.ErrConflict) + conflicted, err = target.Conflicted() + c.Assert(err, gc.IsNil) + c.Assert(conflicted, gc.Equals, true) + checkCleanup(c, s.deployer) + + // Manually resolve, and commit. + err = ioutil.WriteFile(filepath.Join(target.Path(), "some-file"), []byte("nu!"), 0644) + c.Assert(err, gc.IsNil) + err = s.deployer.NotifyResolved() + c.Assert(err, gc.IsNil) + conflicted, err = target.Conflicted() + c.Assert(err, gc.IsNil) + c.Assert(conflicted, gc.Equals, false) + + // Try a final upgrade to the same charm and check it doesn't write anything + // except the upgrade log line. + err = s.deployer.Deploy() + c.Assert(err, gc.IsNil) + checkCleanup(c, s.deployer) + + data, err = ioutil.ReadFile(filepath.Join(target.Path(), "some-file")) + c.Assert(err, gc.IsNil) + c.Assert(string(data), gc.Equals, "nu!") + conflicted, err = target.Conflicted() + c.Assert(err, gc.IsNil) + c.Assert(conflicted, gc.Equals, false) + lines, err := target.Log() + c.Assert(err, gc.IsNil) + c.Assert(lines[0], gc.Matches, `[0-9a-f]{7} Upgraded charm to "cs:s/c-2".`) +} + +func checkCleanup(c *gc.C, d charm.Deployer) { + // Only one update dir should exist and be pointed to by the 'current' + // symlink since extra ones should have been cleaned up by + // cleanupOrphans. + deployerPath := charm.GitDeployerDataPath(d) + updateDirs, err := filepath.Glob(filepath.Join(deployerPath, "update-*")) + c.Assert(err, gc.IsNil) + c.Assert(updateDirs, gc.HasLen, 1) + deployerCurrent := charm.GitDeployerCurrent(d) + current, err := os.Readlink(deployerCurrent.Path()) + c.Assert(err, gc.IsNil) + c.Assert(updateDirs[0], gc.Equals, current) + + // No install dirs should be left behind since the one created is + // renamed to the target path. + installDirs, err := filepath.Glob(filepath.Join(deployerPath, "install-*")) + c.Assert(err, gc.IsNil) + c.Assert(installDirs, gc.HasLen, 0) +} diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/git.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/uniter/charm/git.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/git.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/uniter/charm/git.go 2014-03-27 15:48:42.000000000 +0000 @@ -1,10 +1,9 @@ -// Copyright 2012, 2013 Canonical Ltd. +// Copyright 2012-2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package charm import ( - "errors" "fmt" "io" "io/ioutil" @@ -14,12 +13,8 @@ "strings" "launchpad.net/juju-core/charm" - "launchpad.net/juju-core/log" - "launchpad.net/juju-core/utils" ) -var ErrConflict = errors.New("charm upgrade has conflicts") - // GitDir exposes a specialized subset of git operations on a directory. type GitDir struct { path string @@ -212,7 +207,7 @@ } func (d *GitDir) logError(err error, output string, args ...string) error { - log.Errorf("worker/uniter/charm: git command failed: %s\npath: %s\nargs: %#v\n%s", + logger.Errorf("git command failed: %s\npath: %s\nargs: %#v\n%s", err, d.path, args, output) return fmt.Errorf("git %s failed: %s", args[0], err) } @@ -234,17 +229,13 @@ return statuses, nil } -// ReadCharmURL reads the charm identity file from the supplied GitDir. -func ReadCharmURL(d *GitDir) (*charm.URL, error) { - path := filepath.Join(d.path, ".juju-charm") - surl := "" - if err := utils.ReadYaml(path, &surl); err != nil { - return nil, err - } - return charm.ParseURL(surl) +// ReadCharmURL reads the charm identity file from the GitDir. +func (d *GitDir) ReadCharmURL() (*charm.URL, error) { + path := filepath.Join(d.path, charmURLPath) + return ReadCharmURL(path) } -// WriteCharmURL writes a charm identity file into the directory. -func WriteCharmURL(d *GitDir, url *charm.URL) error { - return utils.WriteYaml(filepath.Join(d.path, ".juju-charm"), url.String()) +// WriteCharmURL writes the charm identity file into the GitDir. +func (d *GitDir) WriteCharmURL(url *charm.URL) error { + return WriteCharmURL(filepath.Join(d.path, charmURLPath), url) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/git_test.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/uniter/charm/git_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/uniter/charm/git_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/uniter/charm/git_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -1,4 +1,4 @@ -// Copyright 2012, 2013 Canonical Ltd. +// Copyright 2012-2014 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. package charm_test @@ -84,7 +84,7 @@ c.Assert(err, gc.IsNil) c.Assert(exists, jc.IsTrue) - _, err = charm.ReadCharmURL(repo) + _, err = repo.ReadCharmURL() c.Assert(err, jc.Satisfies, os.IsNotExist) err = repo.Init() @@ -97,7 +97,7 @@ c.Assert(err, gc.IsNil) err = ioutil.WriteFile(filepath.Join(target.Path(), "initial"), []byte("initial"), 0644) c.Assert(err, gc.IsNil) - err = charm.WriteCharmURL(target, curl) + err = target.WriteCharmURL(curl) c.Assert(err, gc.IsNil) err = target.AddAll() c.Assert(err, gc.IsNil) @@ -113,7 +113,7 @@ source := newRepo(c) err = target.Pull(source) c.Assert(err, gc.IsNil) - url, err := charm.ReadCharmURL(target) + url, err := target.ReadCharmURL() c.Assert(err, gc.IsNil) c.Assert(url, gc.DeepEquals, curl) fi, err := os.Stat(filepath.Join(target.Path(), "some-dir")) diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/upgrader/export_test.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/upgrader/export_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/upgrader/export_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/upgrader/export_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -5,10 +5,11 @@ import ( "launchpad.net/juju-core/tools" + "launchpad.net/juju-core/utils" ) var RetryAfter = &retryAfter -func EnsureTools(u *Upgrader, agentTools *tools.Tools, disableSSLHostnameVerification bool) error { - return u.ensureTools(agentTools, disableSSLHostnameVerification) +func EnsureTools(u *Upgrader, agentTools *tools.Tools, hostnameVerification utils.SSLHostnameVerification) error { + return u.ensureTools(agentTools, hostnameVerification) } diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/upgrader/upgrader.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/upgrader/upgrader.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/upgrader/upgrader.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/upgrader/upgrader.go 2014-03-27 15:48:42.000000000 +0000 @@ -91,10 +91,10 @@ // that we attempt an upgrade even if other workers are dying // all around us. var ( - dying <-chan struct{} - wantTools *coretools.Tools - wantVersion version.Number - disableSSLHostnameVerification bool + dying <-chan struct{} + wantTools *coretools.Tools + wantVersion version.Number + hostnameVerification utils.SSLHostnameVerification ) for { select { @@ -117,7 +117,7 @@ // TODO(dimitern) 2013-10-03 bug #1234715 // Add a testing HTTPS storage to verify the // disableSSLHostnameVerification behavior here. - wantTools, disableSSLHostnameVerification, err = u.st.Tools(u.tag) + wantTools, hostnameVerification, err = u.st.Tools(u.tag) if err != nil { // Not being able to lookup Tools is considered fatal return err @@ -127,7 +127,7 @@ // repeatedly (causing the agent to be stopped), as long // as we have got as far as this, we will still be able to // upgrade the agent. - err := u.ensureTools(wantTools, disableSSLHostnameVerification) + err := u.ensureTools(wantTools, hostnameVerification) if err == nil { return &UpgradeReadyError{ OldTools: version.Current, @@ -142,17 +142,13 @@ } } -func (u *Upgrader) ensureTools(agentTools *coretools.Tools, disableSSLHostnameVerification bool) error { +func (u *Upgrader) ensureTools(agentTools *coretools.Tools, hostnameVerification utils.SSLHostnameVerification) error { if _, err := agenttools.ReadTools(u.dataDir, agentTools.Version); err == nil { // Tools have already been downloaded return nil } - client := http.DefaultClient logger.Infof("fetching tools from %q", agentTools.URL) - if disableSSLHostnameVerification { - logger.Infof("hostname SSL verification disabled") - client = utils.GetNonValidatingHTTPClient() - } + client := utils.GetHTTPClient(hostnameVerification) resp, err := client.Get(agentTools.URL) if err != nil { return err diff -Nru juju-core-1.17.6/src/launchpad.net/juju-core/worker/upgrader/upgrader_test.go juju-core-1.17.7/src/launchpad.net/juju-core/worker/upgrader/upgrader_test.go --- juju-core-1.17.6/src/launchpad.net/juju-core/worker/upgrader/upgrader_test.go 2014-03-20 12:52:38.000000000 +0000 +++ juju-core-1.17.7/src/launchpad.net/juju-core/worker/upgrader/upgrader_test.go 2014-03-27 15:48:42.000000000 +0000 @@ -25,6 +25,7 @@ "launchpad.net/juju-core/state/api" statetesting "launchpad.net/juju-core/state/testing" coretools "launchpad.net/juju-core/tools" + "launchpad.net/juju-core/utils" "launchpad.net/juju-core/version" "launchpad.net/juju-core/worker/upgrader" ) @@ -227,6 +228,6 @@ // it doesn't actually do an HTTP request u := s.makeUpgrader() newTools.URL = "http://localhost:999999/invalid/path/tools.tgz" - err := upgrader.EnsureTools(u, newTools, true) + err := upgrader.EnsureTools(u, newTools, utils.VerifySSLHostnames) c.Assert(err, gc.IsNil) }